A game framework written with osu! in mind.
at master 15 kB view raw
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.Linq; 7using System.Threading; 8using System.Threading.Tasks; 9using NUnit.Framework; 10using NUnit.Framework.Internal; 11using osu.Framework.Development; 12using osu.Framework.Extensions.TypeExtensions; 13using osu.Framework.Graphics; 14using osu.Framework.Graphics.Containers; 15using osu.Framework.Graphics.Shapes; 16using osu.Framework.Graphics.Sprites; 17using osu.Framework.Platform; 18using osu.Framework.Testing.Drawables.Steps; 19using osu.Framework.Threading; 20using osuTK; 21using osuTK.Graphics; 22 23namespace osu.Framework.Testing 24{ 25 [ExcludeFromDynamicCompile] 26 [TestFixture] 27 public abstract class TestScene : Container, IDynamicallyCompile 28 { 29 public readonly FillFlowContainer<Drawable> StepsContainer; 30 private readonly Container content; 31 32 protected override Container<Drawable> Content => content; 33 34 protected virtual ITestSceneTestRunner CreateRunner() => new TestSceneTestRunner(); 35 36 private GameHost host; 37 private Task runTask; 38 private ITestSceneTestRunner runner; 39 40 public object DynamicCompilationOriginal { get; internal set; } 41 42 [OneTimeSetUp] 43 public void SetupGameHost() 44 { 45 host = new TestRunHeadlessGameHost($"{GetType().Name}-{Guid.NewGuid()}"); 46 runner = CreateRunner(); 47 48 if (!(runner is Game game)) 49 throw new InvalidCastException($"The test runner must be a {nameof(Game)}."); 50 51 runTask = Task.Factory.StartNew(() => host.Run(game), TaskCreationOptions.LongRunning); 52 53 while (!game.IsLoaded) 54 { 55 checkForErrors(); 56 Thread.Sleep(10); 57 } 58 } 59 60 protected internal override void AddInternal(Drawable drawable) => 61 throw new InvalidOperationException($"Modifying {nameof(InternalChildren)} will cause critical failure. Use {nameof(Add)} instead."); 62 63 protected internal override void ClearInternal(bool disposeChildren = true) => 64 throw new InvalidOperationException($"Modifying {nameof(InternalChildren)} will cause critical failure. Use {nameof(Clear)} instead."); 65 66 protected internal override bool RemoveInternal(Drawable drawable) => 67 throw new InvalidOperationException($"Modifying {nameof(InternalChildren)} will cause critical failure. Use {nameof(Remove)} instead."); 68 69 [OneTimeTearDown] 70 public void DestroyGameHost() 71 { 72 host.Exit(); 73 74 try 75 { 76 runTask.Wait(); 77 } 78 finally 79 { 80 host.Dispose(); 81 82 try 83 { 84 // clean up after each run 85 host.Storage.DeleteDirectory(string.Empty); 86 } 87 catch 88 { 89 } 90 } 91 } 92 93 [SetUp] 94 public void SetUpTestForNUnit() 95 { 96 if (DebugUtils.IsNUnitRunning) 97 { 98 // Since the host is created in OneTimeSetUp, all game threads will have the fixture's execution context 99 // This is undesirable since each test is run using those same threads, so we must make sure the execution context 100 // for the game threads refers to the current _test_ execution context for each test 101 var executionContext = TestExecutionContext.CurrentContext; 102 103 foreach (var thread in host.Threads) 104 { 105 thread.Scheduler.Add(() => 106 { 107 TestExecutionContext.CurrentContext.CurrentResult = executionContext.CurrentResult; 108 TestExecutionContext.CurrentContext.CurrentTest = executionContext.CurrentTest; 109 TestExecutionContext.CurrentContext.CurrentCulture = executionContext.CurrentCulture; 110 TestExecutionContext.CurrentContext.CurrentPrincipal = executionContext.CurrentPrincipal; 111 TestExecutionContext.CurrentContext.CurrentRepeatCount = executionContext.CurrentRepeatCount; 112 TestExecutionContext.CurrentContext.CurrentUICulture = executionContext.CurrentUICulture; 113 }); 114 } 115 116 if (TestContext.CurrentContext.Test.MethodName != nameof(TestConstructor)) 117 schedule(() => StepsContainer.Clear()); 118 119 RunSetUpSteps(); 120 } 121 } 122 123 [TearDown] 124 public void RunTests() 125 { 126 RunTearDownSteps(); 127 128 checkForErrors(); 129 runner.RunTestBlocking(this); 130 checkForErrors(); 131 } 132 133 private void checkForErrors() 134 { 135 if (host.ExecutionState == ExecutionState.Stopping) 136 runTask.Wait(); 137 138 if (runTask.Exception != null) 139 throw runTask.Exception; 140 } 141 142 /// <summary> 143 /// Tests any steps and assertions in the constructor of this <see cref="TestScene"/>. 144 /// This test must run before any other tests, as it relies on <see cref="StepsContainer"/> not being cleared and not having any elements. 145 /// </summary> 146 [Test, Order(int.MinValue)] 147 public void TestConstructor() 148 { 149 } 150 151 protected TestScene() 152 { 153 DynamicCompilationOriginal = this; 154 155 Name = RemovePrefix(GetType().ReadableName()); 156 157 RelativeSizeAxes = Axes.Both; 158 Masking = true; 159 160 base.AddInternal(new Container 161 { 162 RelativeSizeAxes = Axes.Both, 163 Children = new Drawable[] 164 { 165 new Box 166 { 167 Colour = new Color4(25, 25, 25, 255), 168 RelativeSizeAxes = Axes.Y, 169 Width = steps_width, 170 }, 171 scroll = new BasicScrollContainer 172 { 173 Width = steps_width, 174 Depth = float.MinValue, 175 RelativeSizeAxes = Axes.Y, 176 Child = StepsContainer = new FillFlowContainer<Drawable> 177 { 178 Direction = FillDirection.Vertical, 179 Spacing = new Vector2(3), 180 RelativeSizeAxes = Axes.X, 181 AutoSizeAxes = Axes.Y, 182 Padding = new MarginPadding(10), 183 Child = new SpriteText 184 { 185 Font = FrameworkFont.Condensed.With(size: 16), 186 Text = Name, 187 Margin = new MarginPadding { Bottom = 5 }, 188 } 189 }, 190 }, 191 new Container 192 { 193 Masking = true, 194 Padding = new MarginPadding 195 { 196 Left = steps_width + padding, 197 Right = padding, 198 Top = padding, 199 Bottom = padding, 200 }, 201 RelativeSizeAxes = Axes.Both, 202 Child = content = new DrawFrameRecordingContainer 203 { 204 Masking = true, 205 RelativeSizeAxes = Axes.Both 206 } 207 }, 208 } 209 }); 210 } 211 212 private const float steps_width = 180; 213 private const float padding = 0; 214 215 private int actionIndex; 216 private int actionRepetition; 217 private ScheduledDelegate stepRunner; 218 private readonly ScrollContainer<Drawable> scroll; 219 220 public void RunAllSteps(Action onCompletion = null, Action<Exception> onError = null, Func<StepButton, bool> stopCondition = null, StepButton startFromStep = null) 221 { 222 // schedule once as we want to ensure we have run our LoadComplete before attempting to execute steps. 223 // a user may be adding a step in LoadComplete. 224 Schedule(() => 225 { 226 stepRunner?.Cancel(); 227 foreach (var step in StepsContainer.FlowingChildren.OfType<StepButton>()) 228 step.Reset(); 229 230 actionIndex = startFromStep != null ? StepsContainer.IndexOf(startFromStep) + 1 : -1; 231 actionRepetition = 0; 232 runNextStep(onCompletion, onError, stopCondition); 233 }); 234 } 235 236 private StepButton loadableStep => actionIndex >= 0 ? StepsContainer.Children.ElementAtOrDefault(actionIndex) as StepButton : null; 237 238 protected virtual double TimePerAction => 200; 239 240 private void runNextStep(Action onCompletion, Action<Exception> onError, Func<StepButton, bool> stopCondition) 241 { 242 try 243 { 244 if (loadableStep != null) 245 { 246 scroll.ScrollIntoView(loadableStep); 247 loadableStep.PerformStep(); 248 } 249 } 250 catch (Exception e) 251 { 252 onError?.Invoke(e); 253 return; 254 } 255 256 string text = "."; 257 258 if (actionRepetition == 0) 259 { 260 text = $"{(int)Time.Current}: ".PadLeft(7); 261 262 if (actionIndex < 0) 263 text += $"{GetType().ReadableName()}"; 264 else 265 text += $"step {actionIndex + 1} {loadableStep?.ToString() ?? string.Empty}"; 266 } 267 268 Console.Write(text); 269 270 actionRepetition++; 271 272 if (actionRepetition > (loadableStep?.RequiredRepetitions ?? 1) - 1) 273 { 274 actionIndex++; 275 actionRepetition = 0; 276 Console.WriteLine(); 277 278 if (loadableStep != null && stopCondition?.Invoke(loadableStep) == true) 279 return; 280 } 281 282 if (actionIndex > StepsContainer.Children.Count - 1) 283 { 284 onCompletion?.Invoke(); 285 return; 286 } 287 288 if (Parent != null) 289 stepRunner = Scheduler.AddDelayed(() => runNextStep(onCompletion, onError, stopCondition), TimePerAction); 290 } 291 292 public void AddStep(StepButton step) => schedule(() => StepsContainer.Add(step)); 293 294 private bool addStepsAsSetupSteps; 295 296 public StepButton AddStep(string description, Action action) 297 { 298 var step = new SingleStepButton(addStepsAsSetupSteps) 299 { 300 Text = description, 301 Action = action 302 }; 303 304 AddStep(step); 305 306 return step; 307 } 308 309 public LabelStep AddLabel(string description) 310 { 311 var step = new LabelStep 312 { 313 Text = description, 314 }; 315 316 step.Action = () => 317 { 318 // kinda hacky way to avoid this doesn't get triggered by automated runs. 319 if (step.IsHovered) 320 RunAllSteps(startFromStep: step, stopCondition: s => s is LabelStep); 321 }; 322 323 AddStep(step); 324 325 return step; 326 } 327 328 protected void AddRepeatStep(string description, Action action, int invocationCount) => schedule(() => 329 { 330 StepsContainer.Add(new RepeatStepButton(action, invocationCount, addStepsAsSetupSteps) 331 { 332 Text = description, 333 }); 334 }); 335 336 protected void AddToggleStep(string description, Action<bool> action) => schedule(() => 337 { 338 StepsContainer.Add(new ToggleStepButton(action) 339 { 340 Text = description 341 }); 342 }); 343 344 protected void AddUntilStep(string description, Func<bool> waitUntilTrueDelegate) => schedule(() => 345 { 346 StepsContainer.Add(new UntilStepButton(waitUntilTrueDelegate, addStepsAsSetupSteps) 347 { 348 Text = description ?? @"Until", 349 }); 350 }); 351 352 protected void AddWaitStep(string description, int waitCount) => schedule(() => 353 { 354 StepsContainer.Add(new RepeatStepButton(() => { }, waitCount, addStepsAsSetupSteps) 355 { 356 Text = description ?? @"Wait", 357 }); 358 }); 359 360 protected void AddSliderStep<T>(string description, T min, T max, T start, Action<T> valueChanged) where T : struct, IComparable<T>, IConvertible, IEquatable<T> => schedule(() => 361 { 362 StepsContainer.Add(new StepSlider<T>(description, min, max, start) 363 { 364 ValueChanged = valueChanged, 365 }); 366 }); 367 368 protected void AddAssert(string description, Func<bool> assert, string extendedDescription = null) => schedule(() => 369 { 370 StepsContainer.Add(new AssertButton(addStepsAsSetupSteps) 371 { 372 Text = description, 373 ExtendedDescription = extendedDescription, 374 CallStack = new StackTrace(1), 375 Assertion = assert, 376 }); 377 }); 378 379 internal void RunSetUpSteps() 380 { 381 addStepsAsSetupSteps = true; 382 foreach (var method in ReflectionUtils.GetMethodsWithAttribute(GetType(), typeof(SetUpStepsAttribute), true)) 383 method.Invoke(this, null); 384 addStepsAsSetupSteps = false; 385 } 386 387 internal void RunTearDownSteps() 388 { 389 foreach (var method in ReflectionUtils.GetMethodsWithAttribute(GetType(), typeof(TearDownStepsAttribute), true)) 390 method.Invoke(this, null); 391 } 392 393 /// <summary> 394 /// Remove the "TestScene" prefix from a name. 395 /// </summary> 396 /// <param name="name"></param> 397 public static string RemovePrefix(string name) 398 { 399 return name.Replace("TestCase", string.Empty) // TestScene used to be called TestCase. This handles consumer projects which haven't updated their naming for the near future. 400 .Replace(nameof(TestScene), string.Empty); 401 } 402 403 // should run inline where possible. this is to fix RunAllSteps potentially finding no steps if the steps are added in LoadComplete (else they get forcefully scheduled too late) 404 private void schedule(Action action) => Scheduler.Add(action, false); 405 } 406}