A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System;
5using System.Diagnostics;
6using System.Runtime.ExceptionServices;
7using System.Threading;
8using osu.Framework.Allocation;
9using osu.Framework.Bindables;
10using osu.Framework.Configuration;
11using osu.Framework.Graphics;
12using osu.Framework.Graphics.Containers;
13using osu.Framework.Platform;
14
15namespace osu.Framework.Testing
16{
17 public class TestSceneTestRunner : Game, ITestSceneTestRunner
18 {
19 private readonly TestRunner runner;
20
21 public TestSceneTestRunner()
22 {
23 Add(runner = new TestRunner());
24 }
25
26 /// <summary>
27 /// Blocks execution until a provided <see cref="TestScene"/> runs to completion.
28 /// </summary>
29 /// <param name="test">The <see cref="TestScene"/> to run.</param>
30 public virtual void RunTestBlocking(TestScene test) => runner.RunTestBlocking(test);
31
32 public class TestRunner : CompositeDrawable
33 {
34 private const double time_between_tests = 200;
35
36 private Bindable<double> volume;
37 private double volumeAtStartup;
38
39 [Resolved]
40 private GameHost host { get; set; }
41
42 public TestRunner()
43 {
44 RelativeSizeAxes = Axes.Both;
45 }
46
47 [BackgroundDependencyLoader]
48 private void load(FrameworkConfigManager config)
49 {
50 volume = config.GetBindable<double>(FrameworkSetting.VolumeUniversal);
51 volumeAtStartup = volume.Value;
52 volume.Value = 0;
53 }
54
55 internal override void UnbindAllBindables()
56 {
57 base.UnbindAllBindables();
58 if (volume != null) volume.Value = volumeAtStartup;
59 }
60
61 /// <summary>
62 /// Blocks execution until a provided <see cref="TestScene"/> runs to completion.
63 /// </summary>
64 /// <param name="test">The <see cref="TestScene"/> to run.</param>
65 public void RunTestBlocking(TestScene test)
66 {
67 Trace.Assert(host != null, $"Ensure this runner has been loaded before calling {nameof(RunTestBlocking)}");
68
69 bool completed = false;
70 ExceptionDispatchInfo exception = null;
71
72 void complete()
73 {
74 // We want to remove the TestScene from the hierarchy on completion as under nUnit, it may have operations run on it from a different thread.
75 // This is because nUnit will reuse the same class multiple times, running a different [Test] method each time, while the GameHost
76 // is run from its own asynchronous thread.
77 RemoveInternal(test);
78 completed = true;
79 }
80
81 Schedule(() =>
82 {
83 AddInternal(test);
84
85 Console.WriteLine($@"{(int)Time.Current}: Running {test} visual test cases...");
86
87 // Nunit will run the tests in the TestScene with the same TestScene instance so the TestScene
88 // needs to be removed before the host is exited, otherwise it will end up disposed
89
90 test.RunAllSteps(() =>
91 {
92 Scheduler.AddDelayed(complete, time_between_tests);
93 }, e =>
94 {
95 exception = ExceptionDispatchInfo.Capture(e);
96 complete();
97 });
98 });
99
100 while (!completed && host.ExecutionState == ExecutionState.Running)
101 Thread.Sleep(10);
102
103 exception?.Throw();
104 }
105 }
106 }
107
108 public interface ITestSceneTestRunner
109 {
110 /// <summary>
111 /// Blocks execution until a provided <see cref="TestScene"/> runs to completion.
112 /// </summary>
113 /// <param name="test">The <see cref="TestScene"/> to run.</param>
114 void RunTestBlocking(TestScene test);
115 }
116}