A game framework written with osu! in mind.
at master 752 lines 29 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.Collections; 6using System.Collections.Generic; 7using System.Diagnostics; 8using System.Linq; 9using System.Reflection; 10using NUnit.Framework; 11using NUnit.Framework.Internal; 12using osu.Framework.Allocation; 13using osu.Framework.Audio; 14using osu.Framework.Bindables; 15using osu.Framework.Configuration; 16using osu.Framework.Development; 17using osu.Framework.Extensions; 18using osu.Framework.Extensions.IEnumerableExtensions; 19using osu.Framework.Extensions.ObjectExtensions; 20using osu.Framework.Extensions.TypeExtensions; 21using osu.Framework.Graphics; 22using osu.Framework.Graphics.Containers; 23using osu.Framework.Graphics.Shapes; 24using osu.Framework.Graphics.Sprites; 25using osu.Framework.Graphics.UserInterface; 26using osu.Framework.Input; 27using osu.Framework.Input.Bindings; 28using osu.Framework.Input.Events; 29using osu.Framework.IO.Stores; 30using osu.Framework.Platform; 31using osu.Framework.Testing.Drawables; 32using osu.Framework.Testing.Drawables.Steps; 33using osu.Framework.Timing; 34using osuTK; 35using osuTK.Graphics; 36using osuTK.Input; 37using Logger = osu.Framework.Logging.Logger; 38 39namespace osu.Framework.Testing 40{ 41 [Cached] 42 public class TestBrowser : KeyBindingContainer<TestBrowserAction>, IKeyBindingHandler<TestBrowserAction>, IHandleGlobalKeyboardInput 43 { 44 public TestScene CurrentTest { get; private set; } 45 46 private BasicTextBox searchTextBox; 47 private SearchContainer<TestGroupButton> leftFlowContainer; 48 private Container testContentContainer; 49 private Container compilingNotice; 50 51 public readonly List<Type> TestTypes = new List<Type>(); 52 53 private ConfigManager<TestBrowserSetting> config; 54 55 private DynamicClassCompiler<TestScene> backgroundCompiler; 56 57 private bool interactive; 58 59 private readonly List<Assembly> assemblies; 60 61 /// <summary> 62 /// Creates a new TestBrowser that displays the TestCases of every assembly that start with either "osu" or the specified namespace (if it isn't null) 63 /// </summary> 64 /// <param name="assemblyNamespace">Assembly prefix which is used to match assemblies whose tests should be displayed</param> 65 public TestBrowser(string assemblyNamespace = null) 66 { 67 assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(n => 68 { 69 Debug.Assert(n.FullName != null); 70 return n.FullName.StartsWith("osu", StringComparison.Ordinal) || assemblyNamespace != null && n.FullName.StartsWith(assemblyNamespace, StringComparison.Ordinal); 71 }).ToList(); 72 73 //we want to build the lists here because we're interested in the assembly we were *created* on. 74 foreach (Assembly asm in assemblies.ToList()) 75 { 76 var tests = asm.GetLoadableTypes().Where(isValidVisualTest).ToList(); 77 78 if (!tests.Any()) 79 { 80 assemblies.Remove(asm); 81 continue; 82 } 83 84 foreach (Type type in tests) 85 TestTypes.Add(type); 86 } 87 88 TestTypes.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); 89 } 90 91 private bool isValidVisualTest(Type t) => t.IsSubclassOf(typeof(TestScene)) && !t.IsAbstract && t.IsPublic && !t.GetCustomAttributes<HeadlessTestAttribute>().Any(); 92 93 private void updateList(ValueChangedEvent<Assembly> args) 94 { 95 leftFlowContainer.Clear(); 96 97 //Add buttons for each TestCase. 98 string namespacePrefix = TestTypes.Select(t => t.Namespace).GetCommonPrefix(); 99 100 leftFlowContainer.AddRange(TestTypes.Where(t => t.Assembly == args.NewValue) 101 .GroupBy( 102 t => 103 { 104 string group = t.Namespace?.AsSpan(namespacePrefix.Length).TrimStart('.').ToString(); 105 return string.IsNullOrWhiteSpace(group) ? "Ungrouped" : group; 106 }, 107 t => t, 108 (group, types) => new TestGroup { Name = group, TestTypes = types.ToArray() } 109 ).OrderBy(g => g.Name) 110 .Select(t => new TestGroupButton(type => LoadTest(type), t))); 111 } 112 113 internal readonly BindableDouble PlaybackRate = new BindableDouble(1) { MinValue = 0, MaxValue = 2, Default = 1 }; 114 internal readonly Bindable<Assembly> Assembly = new Bindable<Assembly>(); 115 internal readonly Bindable<bool> RunAllSteps = new Bindable<bool>(); 116 internal readonly Bindable<RecordState> RecordState = new Bindable<RecordState>(); 117 internal readonly BindableInt CurrentFrame = new BindableInt { MinValue = 0, MaxValue = 0 }; 118 119 private TestBrowserToolbar toolbar; 120 private Container leftContainer; 121 private Container mainContainer; 122 123 private const float test_list_width = 200; 124 125 private Action exit; 126 127 private readonly BindableDouble audioRateAdjust = new BindableDouble(1); 128 129 [BackgroundDependencyLoader] 130 private void load(Storage storage, GameHost host, FrameworkConfigManager frameworkConfig, FontStore fonts, AudioManager audio) 131 { 132 interactive = host.Window != null; 133 config = new TestBrowserConfig(storage); 134 135 exit = host.Exit; 136 137 audio.AddAdjustment(AdjustableProperty.Frequency, audioRateAdjust); 138 139 var rateAdjustClock = new StopwatchClock(true); 140 var framedClock = new FramedClock(rateAdjustClock); 141 142 Children = new Drawable[] 143 { 144 mainContainer = new Container 145 { 146 RelativeSizeAxes = Axes.Both, 147 Padding = new MarginPadding { Left = test_list_width }, 148 Children = new Drawable[] 149 { 150 new SafeAreaContainer 151 { 152 SafeAreaOverrideEdges = Edges.Right | Edges.Bottom, 153 RelativeSizeAxes = Axes.Both, 154 Child = testContentContainer = new Container 155 { 156 Clock = framedClock, 157 RelativeSizeAxes = Axes.Both, 158 Padding = new MarginPadding { Top = 50 }, 159 Child = compilingNotice = new Container 160 { 161 Alpha = 0, 162 Anchor = Anchor.Centre, 163 Origin = Anchor.Centre, 164 Masking = true, 165 Depth = float.MinValue, 166 CornerRadius = 5, 167 AutoSizeAxes = Axes.Both, 168 Children = new Drawable[] 169 { 170 new Box 171 { 172 RelativeSizeAxes = Axes.Both, 173 Colour = Color4.Black, 174 }, 175 new SpriteText 176 { 177 Font = FrameworkFont.Regular.With(size: 30), 178 Text = @"Compiling new version..." 179 } 180 }, 181 } 182 } 183 }, 184 toolbar = new TestBrowserToolbar 185 { 186 RelativeSizeAxes = Axes.X, 187 Height = 50, 188 }, 189 } 190 }, 191 leftContainer = new Container 192 { 193 RelativeSizeAxes = Axes.Y, 194 Size = new Vector2(test_list_width, 1), 195 Masking = true, 196 Children = new Drawable[] 197 { 198 new SafeAreaContainer 199 { 200 SafeAreaOverrideEdges = Edges.Left | Edges.Top | Edges.Bottom, 201 RelativeSizeAxes = Axes.Both, 202 Child = new Box 203 { 204 Colour = FrameworkColour.GreenDark, 205 RelativeSizeAxes = Axes.Both 206 } 207 }, 208 new FillFlowContainer 209 { 210 Direction = FillDirection.Vertical, 211 RelativeSizeAxes = Axes.Both, 212 Children = new Drawable[] 213 { 214 searchTextBox = new TestBrowserTextBox 215 { 216 Height = 25, 217 RelativeSizeAxes = Axes.X, 218 PlaceholderText = "type to search", 219 Depth = -1, 220 }, 221 new BasicScrollContainer 222 { 223 RelativeSizeAxes = Axes.Both, 224 Masking = false, 225 Child = leftFlowContainer = new SearchContainer<TestGroupButton> 226 { 227 Padding = new MarginPadding { Top = 3, Bottom = 20 }, 228 Direction = FillDirection.Vertical, 229 AutoSizeAxes = Axes.Y, 230 RelativeSizeAxes = Axes.X, 231 } 232 } 233 } 234 } 235 } 236 }, 237 }; 238 239 searchTextBox.OnCommit += delegate 240 { 241 var firstTest = leftFlowContainer.Where(b => b.IsPresent).SelectMany(b => b.FilterableChildren).OfType<TestSubButton>() 242 .FirstOrDefault(b => b.MatchingFilter)?.TestType; 243 if (firstTest != null) 244 LoadTest(firstTest); 245 }; 246 247 searchTextBox.Current.ValueChanged += e => leftFlowContainer.SearchTerm = e.NewValue; 248 249 if (RuntimeInfo.IsDesktop) 250 { 251 backgroundCompiler = new DynamicClassCompiler<TestScene>(); 252 backgroundCompiler.CompilationStarted += compileStarted; 253 backgroundCompiler.CompilationFinished += compileFinished; 254 backgroundCompiler.CompilationFailed += compileFailed; 255 256 try 257 { 258 backgroundCompiler.Start(); 259 } 260 catch 261 { 262 //it's okay for this to fail for now. 263 } 264 } 265 266 foreach (Assembly asm in assemblies) 267 toolbar.AddAssembly(asm.GetName().Name, asm); 268 269 Assembly.BindValueChanged(updateList); 270 RunAllSteps.BindValueChanged(v => runTests(null)); 271 PlaybackRate.BindValueChanged(e => 272 { 273 rateAdjustClock.Rate = e.NewValue; 274 audioRateAdjust.Value = e.NewValue; 275 }, true); 276 } 277 278 protected override void Dispose(bool isDisposing) 279 { 280 base.Dispose(isDisposing); 281 backgroundCompiler?.Dispose(); 282 } 283 284 private void compileStarted() => Schedule(() => 285 { 286 compilingNotice.Show(); 287 compilingNotice.FadeColour(Color4.White); 288 }); 289 290 private void compileFailed(Exception ex) => Schedule(() => 291 { 292 Logger.Error(ex, "Error with dynamic compilation!"); 293 294 compilingNotice.FadeIn(100, Easing.OutQuint).Then().FadeOut(800, Easing.InQuint); 295 compilingNotice.FadeColour(Color4.Red, 100); 296 }); 297 298 private void compileFinished(Type newType) => Schedule(() => 299 { 300 compilingNotice.FadeOut(800, Easing.InQuint); 301 compilingNotice.FadeColour(Color4.YellowGreen, 100); 302 303 if (newType == null) 304 return; 305 306 int i = TestTypes.FindIndex(t => t.Name == newType.Name && t.Assembly.GetName().Name == newType.Assembly.GetName().Name); 307 308 if (i < 0) 309 TestTypes.Add(newType); 310 else 311 TestTypes[i] = newType; 312 313 try 314 { 315 LoadTest(newType, isDynamicLoad: true); 316 } 317 catch (Exception e) 318 { 319 compileFailed(e); 320 } 321 }); 322 323 protected override void LoadComplete() 324 { 325 base.LoadComplete(); 326 327 if (CurrentTest == null) 328 { 329 var lastTest = config.Get<string>(TestBrowserSetting.LastTest); 330 331 var foundTest = TestTypes.Find(t => t.FullName == lastTest); 332 333 LoadTest(foundTest); 334 } 335 } 336 337 private void toggleTestList() 338 { 339 if (leftContainer.Width > 0) 340 { 341 leftContainer.Width = 0; 342 mainContainer.Padding = new MarginPadding(); 343 } 344 else 345 { 346 leftContainer.Width = test_list_width; 347 mainContainer.Padding = new MarginPadding { Left = test_list_width }; 348 } 349 } 350 351 protected override bool OnKeyDown(KeyDownEvent e) 352 { 353 if (!e.Repeat) 354 { 355 switch (e.Key) 356 { 357 case Key.Escape: 358 exit(); 359 return true; 360 } 361 } 362 363 return base.OnKeyDown(e); 364 } 365 366 public override IEnumerable<IKeyBinding> DefaultKeyBindings => new[] 367 { 368 new KeyBinding(new[] { InputKey.Control, InputKey.F }, TestBrowserAction.Search), 369 new KeyBinding(new[] { InputKey.Control, InputKey.R }, TestBrowserAction.Reload), // for macOS 370 371 new KeyBinding(new[] { InputKey.Super, InputKey.F }, TestBrowserAction.Search), // for macOS 372 new KeyBinding(new[] { InputKey.Super, InputKey.R }, TestBrowserAction.Reload), // for macOS 373 374 new KeyBinding(new[] { InputKey.Control, InputKey.H }, TestBrowserAction.ToggleTestList), 375 }; 376 377 public bool OnPressed(TestBrowserAction action) 378 { 379 switch (action) 380 { 381 case TestBrowserAction.Search: 382 if (leftContainer.Width == 0) toggleTestList(); 383 GetContainingInputManager().ChangeFocus(searchTextBox); 384 return true; 385 386 case TestBrowserAction.Reload: 387 LoadTest(CurrentTest.GetType()); 388 return true; 389 390 case TestBrowserAction.ToggleTestList: 391 toggleTestList(); 392 return true; 393 } 394 395 return false; 396 } 397 398 public void OnReleased(TestBrowserAction action) 399 { 400 } 401 402 public void LoadTest(Type testType = null, Action onCompletion = null, bool isDynamicLoad = false) 403 { 404 if (CurrentTest?.Parent != null) 405 { 406 testContentContainer.Remove(CurrentTest.Parent); 407 CurrentTest.Dispose(); 408 } 409 410 var lastTest = CurrentTest; 411 412 CurrentTest = null; 413 414 if (testType == null && TestTypes.Count > 0) 415 testType = TestTypes[0]; 416 417 config.SetValue(TestBrowserSetting.LastTest, testType?.FullName ?? string.Empty); 418 419 if (testType == null) 420 return; 421 422 var newTest = (TestScene)Activator.CreateInstance(testType); 423 424 Debug.Assert(newTest != null); 425 426 const string dynamic_prefix = "dynamic"; 427 428 // if we are a dynamically compiled type (via DynamicClassCompiler) we should update the dropdown accordingly. 429 if (isDynamicLoad) 430 { 431 newTest.DynamicCompilationOriginal = lastTest?.DynamicCompilationOriginal ?? lastTest ?? newTest; 432 toolbar.AddAssembly($"{dynamic_prefix} ({testType.Name})", testType.Assembly); 433 } 434 else 435 { 436 TestTypes.RemoveAll(t => 437 { 438 Debug.Assert(t.Assembly.FullName != null); 439 return t.Assembly.FullName.Contains(dynamic_prefix); 440 }); 441 442 newTest.DynamicCompilationOriginal = newTest; 443 } 444 445 Assembly.Value = testType.Assembly; 446 447 CurrentTest = newTest; 448 CurrentTest.OnLoadComplete += _ => Schedule(() => finishLoad(newTest, onCompletion)); 449 450 updateButtons(); 451 resetRecording(); 452 453 testContentContainer.Add(new ErrorCatchingDelayedLoadWrapper(CurrentTest, isDynamicLoad) 454 { 455 OnCaughtError = compileFailed 456 }); 457 } 458 459 private void resetRecording() 460 { 461 CurrentFrame.Value = 0; 462 if (RecordState.Value == Testing.RecordState.Stopped) 463 RecordState.Value = Testing.RecordState.Normal; 464 } 465 466 private void finishLoad(TestScene newTest, Action onCompletion) 467 { 468 if (CurrentTest != newTest) 469 { 470 // There could have been multiple loads fired after us. In such a case we want to silently remove ourselves. 471 testContentContainer.Remove(newTest.Parent); 472 return; 473 } 474 475 updateButtons(); 476 477 bool hadTestAttributeTest = false; 478 479 foreach (var m in newTest.GetType().GetMethods()) 480 { 481 var name = m.Name; 482 483 if (name == nameof(TestScene.TestConstructor) || m.GetCustomAttribute(typeof(IgnoreAttribute), false) != null) 484 continue; 485 486 if (name.StartsWith("Test", StringComparison.Ordinal)) 487 name = name.Substring(4); 488 489 int runCount = 1; 490 491 if (m.GetCustomAttribute(typeof(RepeatAttribute), false) != null) 492 { 493 var count = m.GetCustomAttributesData().Single(a => a.AttributeType == typeof(RepeatAttribute)).ConstructorArguments.Single().Value; 494 Debug.Assert(count != null); 495 496 runCount += (int)count; 497 } 498 499 for (int i = 0; i < runCount; i++) 500 { 501 string repeatSuffix = i > 0 ? $" ({i + 1})" : string.Empty; 502 503 var methodWrapper = new MethodWrapper(m.GetType(), m); 504 505 if (methodWrapper.GetCustomAttributes<TestAttribute>(false).SingleOrDefault() != null) 506 { 507 var parameters = m.GetParameters(); 508 509 if (parameters.Length > 0) 510 { 511 var valueMatrix = new List<List<object>>(); 512 513 foreach (var p in methodWrapper.GetParameters()) 514 { 515 var valueAttrib = p.GetCustomAttributes<ValuesAttribute>(false).SingleOrDefault(); 516 if (valueAttrib == null) 517 throw new ArgumentException($"Parameter is present on a {nameof(TestAttribute)} method without values specification.", p.ParameterInfo.Name); 518 519 List<object> choices = new List<object>(); 520 521 foreach (var choice in valueAttrib.GetData(p)) 522 choices.Add(choice); 523 524 valueMatrix.Add(choices); 525 } 526 527 foreach (var combination in valueMatrix.CartesianProduct()) 528 { 529 hadTestAttributeTest = true; 530 CurrentTest.AddLabel($"{name}({string.Join(", ", combination)}){repeatSuffix}"); 531 handleTestMethod(m, combination.ToArray()); 532 } 533 } 534 else 535 { 536 hadTestAttributeTest = true; 537 CurrentTest.AddLabel($"{name}{repeatSuffix}"); 538 handleTestMethod(m); 539 } 540 } 541 542 foreach (var tc in m.GetCustomAttributes(typeof(TestCaseAttribute), false).OfType<TestCaseAttribute>()) 543 { 544 hadTestAttributeTest = true; 545 CurrentTest.AddLabel($"{name}({string.Join(", ", tc.Arguments)}){repeatSuffix}"); 546 547 handleTestMethod(m, tc.Arguments); 548 } 549 550 foreach (var tcs in m.GetCustomAttributes(typeof(TestCaseSourceAttribute), false).OfType<TestCaseSourceAttribute>()) 551 { 552 IEnumerable sourceValue = getTestCaseSourceValue(m, tcs); 553 554 if (sourceValue == null) 555 { 556 Debug.Assert(tcs.SourceName != null); 557 throw new InvalidOperationException($"The value of the source member {tcs.SourceName} must be non-null."); 558 } 559 560 foreach (var argument in sourceValue) 561 { 562 hadTestAttributeTest = true; 563 564 if (argument is IEnumerable argumentsEnumerable) 565 { 566 var arguments = argumentsEnumerable.Cast<object>().ToArray(); 567 568 CurrentTest.AddLabel($"{name}({string.Join(", ", arguments)}){repeatSuffix}"); 569 handleTestMethod(m, arguments); 570 } 571 else 572 { 573 CurrentTest.AddLabel($"{name}({argument}){repeatSuffix}"); 574 handleTestMethod(m, argument); 575 } 576 } 577 } 578 } 579 } 580 581 // even if no [Test] or [TestCase] methods were found, [SetUp] steps should be added. 582 if (!hadTestAttributeTest) 583 addSetUpSteps(); 584 585 backgroundCompiler?.SetRecompilationTarget(CurrentTest); 586 runTests(onCompletion); 587 updateButtons(); 588 589 void addSetUpSteps() 590 { 591 var setUpMethods = ReflectionUtils.GetMethodsWithAttribute(newTest.GetType(), typeof(SetUpAttribute), true) 592 .Where(m => m.Name != nameof(TestScene.SetUpTestForNUnit)); 593 594 if (setUpMethods.Any()) 595 { 596 CurrentTest.AddStep(new SingleStepButton(true) 597 { 598 Text = "[SetUp]", 599 LightColour = Color4.Teal, 600 Action = () => setUpMethods.ForEach(s => s.Invoke(CurrentTest, null)) 601 }); 602 } 603 604 CurrentTest.RunSetUpSteps(); 605 } 606 607 void handleTestMethod(MethodInfo methodInfo, params object[] arguments) 608 { 609 addSetUpSteps(); 610 methodInfo.Invoke(CurrentTest, arguments); 611 CurrentTest.RunTearDownSteps(); 612 } 613 } 614 615 private static IEnumerable getTestCaseSourceValue(MethodInfo testMethod, TestCaseSourceAttribute tcs) 616 { 617 var sourceDeclaringType = tcs.SourceType ?? testMethod.DeclaringType; 618 Debug.Assert(sourceDeclaringType != null); 619 620 if (tcs.SourceType != null && tcs.SourceName == null) 621 return (IEnumerable)Activator.CreateInstance(tcs.SourceType); 622 623 var sourceMembers = sourceDeclaringType.AsNonNull().GetMember(tcs.SourceName, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy); 624 if (sourceMembers.Length == 0) 625 throw new InvalidOperationException($"No static member with the name of {tcs.SourceName} exists in {sourceDeclaringType} or its base types."); 626 627 if (sourceMembers.Length > 1) 628 throw new NotSupportedException($"There are multiple members with the same source name ({tcs.SourceName}) (e.g. method overloads)."); 629 630 var sourceMember = sourceMembers.Single(); 631 632 switch (sourceMember) 633 { 634 case FieldInfo sf: 635 return (IEnumerable)sf.GetValue(null); 636 637 case PropertyInfo sp: 638 if (!sp.CanRead) 639 throw new InvalidOperationException($"The source property {sp.Name} in {sp.DeclaringType.ReadableName()} must have a getter."); 640 641 return (IEnumerable)sp.GetValue(null); 642 643 case MethodInfo sm: 644 var methodParamsLength = sm.GetParameters().Length; 645 if (methodParamsLength != (tcs.MethodParams?.Length ?? 0)) 646 throw new InvalidOperationException($"The given source method parameters count doesn't match the method. (attribute has {tcs.MethodParams?.Length ?? 0}, method has {methodParamsLength})"); 647 648 return (IEnumerable)sm.Invoke(null, tcs.MethodParams); 649 650 default: 651 throw new NotSupportedException($"{sourceMember.MemberType} is not a supported member type for {nameof(TestCaseSourceAttribute)} (must be static field, property or method)"); 652 } 653 } 654 655 private void runTests(Action onCompletion) 656 { 657 int actualStepCount = 0; 658 CurrentTest.RunAllSteps(onCompletion, e => Logger.Log($@"Error on step: {e}"), s => 659 { 660 if (!interactive || RunAllSteps.Value) 661 return false; 662 663 if (actualStepCount > 0) 664 // stop once one actual step has been run. 665 return true; 666 667 if (!s.IsSetupStep && !(s is LabelStep)) 668 actualStepCount++; 669 670 return false; 671 }); 672 } 673 674 private void updateButtons() 675 { 676 foreach (var b in leftFlowContainer.Children) 677 b.Current = CurrentTest.GetType(); 678 } 679 680 private class ErrorCatchingDelayedLoadWrapper : DelayedLoadWrapper 681 { 682 private readonly bool catchErrors; 683 private bool hasCaught; 684 685 public Action<Exception> OnCaughtError; 686 687 public ErrorCatchingDelayedLoadWrapper(Drawable content, bool catchErrors) 688 : base(content, 0) 689 { 690 this.catchErrors = catchErrors; 691 } 692 693 public override bool UpdateSubTree() 694 { 695 try 696 { 697 return base.UpdateSubTree(); 698 } 699 catch (Exception e) 700 { 701 if (!catchErrors) 702 throw; 703 704 // without this we will enter an infinite loading loop (DelayedLoadWrapper will see the child removed below and retry). 705 hasCaught = true; 706 707 OnCaughtError?.Invoke(e); 708 RemoveInternal(Content); 709 } 710 711 return false; 712 } 713 714 protected override bool ShouldLoadContent => !hasCaught; 715 } 716 717 private class TestBrowserTextBox : BasicTextBox 718 { 719 protected override float LeftRightPadding => TestButtonBase.LEFT_TEXT_PADDING; 720 721 public TestBrowserTextBox() 722 { 723 TextFlow.Height = 0.75f; 724 } 725 } 726 } 727 728 internal enum RecordState 729 { 730 /// <summary> 731 /// The game is playing back normally. 732 /// </summary> 733 Normal, 734 735 /// <summary> 736 /// Drawn game frames are currently being recorded. 737 /// </summary> 738 Recording, 739 740 /// <summary> 741 /// The default game playback is stopped, recorded frames are being played back. 742 /// </summary> 743 Stopped 744 } 745 746 public enum TestBrowserAction 747 { 748 ToggleTestList, 749 Reload, 750 Search 751 } 752}