ScenarioTests.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. using System.Diagnostics;
  2. using System.Reflection;
  3. using System.Runtime.InteropServices;
  4. using UICatalog;
  5. using UnitTests;
  6. using Xunit.Abstractions;
  7. namespace IntegrationTests.UICatalog;
  8. public class ScenarioTests : TestsAllViews
  9. {
  10. public ScenarioTests (ITestOutputHelper output)
  11. {
  12. #if DEBUG_IDISPOSABLE
  13. View.EnableDebugIDisposableAsserts = true;
  14. View.Instances.Clear ();
  15. #endif
  16. _output = output;
  17. }
  18. private readonly ITestOutputHelper _output;
  19. /// <summary>
  20. /// <para>This runs through all Scenarios defined in UI Catalog, calling Init, Setup, and Run.</para>
  21. /// <para>Should find any Scenarios which crash on load or do not respond to <see cref="Application.RequestStop()"/>.</para>
  22. /// </summary>
  23. [Theory]
  24. [MemberData (nameof (AllScenarioTypes))]
  25. public void All_Scenarios_Quit_And_Init_Shutdown_Properly (Type scenarioType)
  26. {
  27. // Disable on Mac due to random failures related to timing issues
  28. if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX))
  29. {
  30. _output.WriteLine ($"Skipping Scenario '{scenarioType}' on macOS due to random timeout failures.");
  31. return;
  32. }
  33. // Force a complete reset
  34. ApplicationImpl.SetInstance (null);
  35. CM.Disable (true);
  36. _output.WriteLine ($"Running Scenario '{scenarioType}'");
  37. Scenario? scenario = null;
  38. var scenarioName = string.Empty;
  39. // Do not use Application.AddTimer for out-of-band watchdogs as
  40. // they will be stopped by Shutdown/ResetState.
  41. Timer? watchdogTimer = null;
  42. var timeoutFired = false;
  43. // Increase timeout for macOS - it's consistently slower
  44. uint abortTime = 5000;
  45. if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX))
  46. {
  47. abortTime = 10000;
  48. }
  49. var initialized = false;
  50. var shutdownGracefully = false;
  51. var iterationCount = 0;
  52. Key quitKey = Application.QuitKey;
  53. // Track if we've already unsubscribed to prevent double-removal
  54. var iterationHandlerRemoved = false;
  55. try
  56. {
  57. scenario = Activator.CreateInstance (scenarioType) as Scenario;
  58. scenarioName = scenario!.GetName ();
  59. Application.InitializedChanged += OnApplicationOnInitializedChanged;
  60. Application.ForceDriver = "FakeDriver";
  61. scenario!.Main ();
  62. Application.ForceDriver = string.Empty;
  63. }
  64. finally
  65. {
  66. // Ensure cleanup happens regardless of how we exit
  67. Application.InitializedChanged -= OnApplicationOnInitializedChanged;
  68. // Remove iteration handler if it wasn't removed
  69. if (!iterationHandlerRemoved)
  70. {
  71. Application.Iteration -= OnApplicationOnIteration;
  72. iterationHandlerRemoved = true;
  73. }
  74. watchdogTimer?.Dispose ();
  75. scenario?.Dispose ();
  76. scenario = null;
  77. ConfigurationManager.Disable (true);
  78. }
  79. Assert.True (initialized, $"Scenario '{scenarioName}' failed to initialize.");
  80. if (timeoutFired)
  81. {
  82. _output.WriteLine ($"WARNING: Scenario '{scenarioName}' timed out after {abortTime}ms. This may indicate a performance issue on this runner.");
  83. }
  84. Assert.True (
  85. shutdownGracefully,
  86. $"Scenario '{scenarioName}' failed to quit with {quitKey} after {abortTime}ms and {iterationCount} iterations. "
  87. + $"TimeoutFired={timeoutFired}");
  88. #if DEBUG_IDISPOSABLE
  89. Assert.Empty (View.Instances);
  90. #endif
  91. return;
  92. void OnApplicationOnInitializedChanged (object? s, EventArgs<bool> a)
  93. {
  94. if (a.Value)
  95. {
  96. Application.Iteration += OnApplicationOnIteration;
  97. initialized = true;
  98. // Use a System.Threading.Timer for the watchdog to ensure it's not affected by Application.StopAllTimers
  99. watchdogTimer = new Timer (_ => ForceCloseCallback (), null, (int)abortTime, System.Threading.Timeout.Infinite);
  100. }
  101. else
  102. {
  103. shutdownGracefully = true;
  104. }
  105. _output.WriteLine ($"Initialized == {a.Value}; shutdownGracefully == {shutdownGracefully}.");
  106. }
  107. // If the scenario doesn't close within abortTime ms, this will force it to quit
  108. void ForceCloseCallback ()
  109. {
  110. timeoutFired = true;
  111. _output.WriteLine ($"TIMEOUT FIRED for {scenarioName} after {abortTime}ms. Attempting graceful shutdown.");
  112. // Don't call ResetState here - let the finally block handle cleanup
  113. // Just try to stop the application gracefully
  114. try
  115. {
  116. if (Application.Initialized)
  117. {
  118. Application.RequestStop ();
  119. }
  120. }
  121. catch (Exception ex)
  122. {
  123. _output.WriteLine ($"Exception during timeout callback: {ex.Message}");
  124. }
  125. }
  126. void OnApplicationOnIteration (object? s, IterationEventArgs a)
  127. {
  128. iterationCount++;
  129. if (Application.Initialized)
  130. {
  131. // Press QuitKey
  132. quitKey = Application.QuitKey;
  133. _output.WriteLine ($"Attempting to quit with {quitKey} after {iterationCount} iterations.");
  134. try
  135. {
  136. Application.RaiseKeyDownEvent (quitKey);
  137. }
  138. catch (Exception ex)
  139. {
  140. _output.WriteLine ($"Exception raising quit key: {ex.Message}");
  141. }
  142. Application.Iteration -= OnApplicationOnIteration;
  143. iterationHandlerRemoved = true;
  144. }
  145. }
  146. }
  147. public static IEnumerable<object []> AllScenarioTypes =>
  148. typeof (Scenario).Assembly
  149. .GetTypes ()
  150. .Where (type => type.IsClass && !type.IsAbstract && type.IsSubclassOf (typeof (Scenario)))
  151. .Select (type => new object [] { type });
  152. [Fact]
  153. public void Run_All_Views_Tester_Scenario ()
  154. {
  155. // Disable any UIConfig settings
  156. ConfigurationManager.Disable (true);
  157. View? curView = null;
  158. // Settings
  159. var xVal = 0;
  160. var yVal = 0;
  161. var wVal = 0;
  162. var hVal = 0;
  163. List<string> posNames = ["Percent", "AnchorEnd", "Center", "Absolute"];
  164. List<string> dimNames = ["Auto", "Percent", "Fill", "Absolute"];
  165. Application.Init ("fake");
  166. var top = new Toplevel ();
  167. Dictionary<string, Type> viewClasses = GetAllViewClasses ().ToDictionary (t => t.Name);
  168. Window leftPane = new ()
  169. {
  170. Title = "Classes",
  171. X = 0,
  172. Y = 0,
  173. Width = 15,
  174. Height = Dim.Fill (1), // for status bar
  175. CanFocus = false,
  176. SchemeName = "TopLevel"
  177. };
  178. ListView classListView = new ()
  179. {
  180. X = 0,
  181. Y = 0,
  182. Width = Dim.Fill (),
  183. Height = Dim.Fill (),
  184. AllowsMarking = false,
  185. SchemeName = "TopLevel",
  186. Source = new ListWrapper<string> (new (viewClasses.Keys.ToList ()))
  187. };
  188. leftPane.Add (classListView);
  189. FrameView settingsPane = new ()
  190. {
  191. X = Pos.Right (leftPane),
  192. Y = 0, // for menu
  193. Width = Dim.Fill (),
  194. Height = 10,
  195. CanFocus = false,
  196. SchemeName = "TopLevel",
  197. Title = "Settings"
  198. };
  199. var radioItems = new [] { "Percent(x)", "AnchorEnd(x)", "Center", "Absolute(x)" };
  200. FrameView locationFrame = new ()
  201. {
  202. X = 0,
  203. Y = 0,
  204. Height = 3 + radioItems.Length,
  205. Width = 36,
  206. Title = "Location (Pos)"
  207. };
  208. settingsPane.Add (locationFrame);
  209. var label = new Label { X = 0, Y = 0, Text = "x:" };
  210. locationFrame.Add (label);
  211. OptionSelector xOptionSelector = new () { X = 0, Y = Pos.Bottom (label), Labels = radioItems };
  212. TextField xText = new () { X = Pos.Right (label) + 1, Y = 0, Width = 4, Text = $"{xVal}" };
  213. locationFrame.Add (xText);
  214. locationFrame.Add (xOptionSelector);
  215. radioItems = new [] { "Percent(y)", "AnchorEnd(y)", "Center", "Absolute(y)" };
  216. label = new () { X = Pos.Right (xOptionSelector) + 1, Y = 0, Text = "y:" };
  217. locationFrame.Add (label);
  218. TextField yText = new () { X = Pos.Right (label) + 1, Y = 0, Width = 4, Text = $"{yVal}" };
  219. locationFrame.Add (yText);
  220. OptionSelector yOptionSelector = new () { X = Pos.X (label), Y = Pos.Bottom (label), Labels = radioItems };
  221. locationFrame.Add (yOptionSelector);
  222. FrameView sizeFrame = new ()
  223. {
  224. X = Pos.Right (locationFrame),
  225. Y = Pos.Y (locationFrame),
  226. Height = 3 + radioItems.Length,
  227. Width = 40,
  228. Title = "Size (Dim)"
  229. };
  230. radioItems = new [] { "Auto()", "Percent(width)", "Fill(width)", "Absolute(width)" };
  231. label = new () { X = 0, Y = 0, Text = "width:" };
  232. sizeFrame.Add (label);
  233. OptionSelector wOptionSelector = new () { X = 0, Y = Pos.Bottom (label), Labels = radioItems };
  234. TextField wText = new () { X = Pos.Right (label) + 1, Y = 0, Width = 4, Text = $"{wVal}" };
  235. sizeFrame.Add (wText);
  236. sizeFrame.Add (wOptionSelector);
  237. radioItems = new [] { "Auto()", "Percent(height)", "Fill(height)", "Absolute(height)" };
  238. label = new () { X = Pos.Right (wOptionSelector) + 1, Y = 0, Text = "height:" };
  239. sizeFrame.Add (label);
  240. TextField hText = new () { X = Pos.Right (label) + 1, Y = 0, Width = 4, Text = $"{hVal}" };
  241. sizeFrame.Add (hText);
  242. OptionSelector hOptionSelector = new () { X = Pos.X (label), Y = Pos.Bottom (label), Labels = radioItems };
  243. sizeFrame.Add (hOptionSelector);
  244. settingsPane.Add (sizeFrame);
  245. FrameView hostPane = new ()
  246. {
  247. X = Pos.Right (leftPane),
  248. Y = Pos.Bottom (settingsPane),
  249. Width = Dim.Fill (),
  250. Height = Dim.Fill (1), // + 1 for status bar
  251. SchemeName = "Dialog"
  252. };
  253. classListView.OpenSelectedItem += (s, a) => { settingsPane.SetFocus (); };
  254. classListView.SelectedItemChanged += (s, args) =>
  255. {
  256. // Remove existing class, if any
  257. if (curView is { })
  258. {
  259. curView.SubViewsLaidOut -= LayoutCompleteHandler;
  260. hostPane.Remove (curView);
  261. curView.Dispose ();
  262. curView = null;
  263. hostPane.FillRect (hostPane.Viewport);
  264. }
  265. curView = CreateClass (viewClasses.Values.ToArray () [classListView.SelectedItem!.Value]);
  266. };
  267. xOptionSelector.ValueChanged += (_, _) => DimPosChanged (curView);
  268. xText.TextChanged += (s, args) =>
  269. {
  270. try
  271. {
  272. xVal = int.Parse (xText.Text);
  273. DimPosChanged (curView);
  274. }
  275. catch
  276. { }
  277. };
  278. yText.TextChanged += (s, e) =>
  279. {
  280. try
  281. {
  282. yVal = int.Parse (yText.Text);
  283. DimPosChanged (curView);
  284. }
  285. catch
  286. { }
  287. };
  288. yOptionSelector.ValueChanged += (_, _) => DimPosChanged (curView);
  289. wOptionSelector.ValueChanged += (_, _) => DimPosChanged (curView);
  290. wText.TextChanged += (s, args) =>
  291. {
  292. try
  293. {
  294. wVal = int.Parse (wText.Text);
  295. DimPosChanged (curView);
  296. }
  297. catch
  298. { }
  299. };
  300. hText.TextChanged += (s, args) =>
  301. {
  302. try
  303. {
  304. hVal = int.Parse (hText.Text);
  305. DimPosChanged (curView);
  306. }
  307. catch
  308. { }
  309. };
  310. hOptionSelector.ValueChanged += (_, _) => DimPosChanged (curView);
  311. top.Add (leftPane, settingsPane, hostPane);
  312. top.LayoutSubViews ();
  313. curView = CreateClass (viewClasses.First ().Value);
  314. var iterations = 0;
  315. Application.Iteration += OnApplicationOnIteration;
  316. Application.Run (top);
  317. Application.Iteration -= OnApplicationOnIteration;
  318. Assert.Equal (viewClasses.Count, iterations);
  319. top.Dispose ();
  320. Application.Shutdown ();
  321. ConfigurationManager.Disable (true);
  322. return;
  323. void OnApplicationOnIteration (object? s, IterationEventArgs a)
  324. {
  325. iterations++;
  326. if (iterations < viewClasses.Count)
  327. {
  328. classListView.MoveDown ();
  329. if (curView is { })
  330. {
  331. Assert.Equal (
  332. curView.GetType ().Name,
  333. viewClasses.Values.ToArray () [classListView.SelectedItem!.Value].Name);
  334. }
  335. }
  336. else
  337. {
  338. Application.RequestStop ();
  339. }
  340. }
  341. void DimPosChanged (View? view)
  342. {
  343. if (view == null)
  344. {
  345. return;
  346. }
  347. try
  348. {
  349. switch (xOptionSelector.Value)
  350. {
  351. case 0:
  352. view.X = Pos.Percent (xVal);
  353. break;
  354. case 1:
  355. view.X = Pos.AnchorEnd (xVal);
  356. break;
  357. case 2:
  358. view.X = Pos.Center ();
  359. break;
  360. case 3:
  361. view.X = Pos.Absolute (xVal);
  362. break;
  363. }
  364. switch (yOptionSelector.Value)
  365. {
  366. case 0:
  367. view.Y = Pos.Percent (yVal);
  368. break;
  369. case 1:
  370. view.Y = Pos.AnchorEnd (yVal);
  371. break;
  372. case 2:
  373. view.Y = Pos.Center ();
  374. break;
  375. case 3:
  376. view.Y = Pos.Absolute (yVal);
  377. break;
  378. }
  379. switch (wOptionSelector.Value)
  380. {
  381. case 0:
  382. view.Width = Dim.Percent (wVal);
  383. break;
  384. case 1:
  385. view.Width = Dim.Fill (wVal);
  386. break;
  387. case 2:
  388. view.Width = Dim.Absolute (wVal);
  389. break;
  390. }
  391. switch (hOptionSelector.Value)
  392. {
  393. case 0:
  394. view.Height = Dim.Percent (hVal);
  395. break;
  396. case 1:
  397. view.Height = Dim.Fill (hVal);
  398. break;
  399. case 2:
  400. view.Height = Dim.Absolute (hVal);
  401. break;
  402. }
  403. }
  404. catch (Exception e)
  405. {
  406. MessageBox.ErrorQuery ("Exception", e.Message, "Ok");
  407. }
  408. UpdateTitle (view);
  409. }
  410. void UpdateSettings (View view)
  411. {
  412. var x = view.X.ToString ();
  413. var y = view.Y.ToString ();
  414. try
  415. {
  416. xOptionSelector.Value = posNames.IndexOf (posNames.First (s => x.Contains (s)));
  417. yOptionSelector.Value = posNames.IndexOf (posNames.First (s => y.Contains (s)));
  418. }
  419. catch (InvalidOperationException e)
  420. {
  421. // This is a hack to work around the fact that the Pos enum doesn't have an "Align" value yet
  422. Debug.WriteLine ($"{e}");
  423. }
  424. xText.Text = $"{view.Frame.X}";
  425. yText.Text = $"{view.Frame.Y}";
  426. var w = view.Width!.ToString ();
  427. var h = view.Height!.ToString ();
  428. wOptionSelector.Value = dimNames.IndexOf (dimNames.First (s => w.Contains (s)));
  429. hOptionSelector.Value = dimNames.IndexOf (dimNames.First (s => h.Contains (s)));
  430. wText.Text = $"{view.Frame.Width}";
  431. hText.Text = $"{view.Frame.Height}";
  432. }
  433. void UpdateTitle (View? view) { hostPane.Title = $"{view!.GetType ().Name} - {view.X}, {view.Y}, {view.Width}, {view.Height}"; }
  434. View? CreateClass (Type type)
  435. {
  436. // If we are to create a generic Type
  437. if (type.IsGenericType)
  438. {
  439. // For each of the <T> arguments
  440. List<Type> typeArguments = new ();
  441. // use <object> or the original type if applicable
  442. foreach (Type arg in type.GetGenericArguments ())
  443. {
  444. if (arg.IsValueType && Nullable.GetUnderlyingType (arg) == null)
  445. {
  446. typeArguments.Add (arg);
  447. }
  448. else
  449. {
  450. typeArguments.Add (typeof (object));
  451. }
  452. }
  453. // Ensure the type does not contain any generic parameters
  454. if (type.ContainsGenericParameters)
  455. {
  456. Logging.Warning ($"Cannot create an instance of {type} because it contains generic parameters.");
  457. //throw new ArgumentException ($"Cannot create an instance of {type} because it contains generic parameters.");
  458. return null;
  459. }
  460. // And change what type we are instantiating from MyClass<T> to MyClass<object>
  461. type = type.MakeGenericType (typeArguments.ToArray ());
  462. }
  463. // Instantiate view
  464. var view = Activator.CreateInstance (type) as View;
  465. if (view is null)
  466. {
  467. return null;
  468. }
  469. if (view.Width is not DimAuto)
  470. {
  471. view.Width = Dim.Percent (75);
  472. }
  473. if (view.Height is not DimAuto)
  474. {
  475. view.Height = Dim.Percent (75);
  476. }
  477. // Set the colorscheme to make it stand out if is null by default
  478. if (!view.HasScheme)
  479. {
  480. view.SchemeName = "Base";
  481. }
  482. // If the view supports a Text property, set it so we have something to look at
  483. if (view.GetType ().GetProperty ("Text") != null)
  484. {
  485. try
  486. {
  487. view.GetType ().GetProperty ("Text")?.GetSetMethod ()?.Invoke (view, new [] { "Test Text" });
  488. }
  489. catch (TargetInvocationException e)
  490. {
  491. MessageBox.ErrorQuery ("Exception", e.InnerException!.Message, "Ok");
  492. view = null;
  493. }
  494. }
  495. // If the view supports a Title property, set it so we have something to look at
  496. if (view != null && view.GetType ().GetProperty ("Title") != null)
  497. {
  498. if (view.GetType ().GetProperty ("Title")!.PropertyType == typeof (string))
  499. {
  500. view?.GetType ().GetProperty ("Title")?.GetSetMethod ()?.Invoke (view, new [] { "Test Title" });
  501. }
  502. else
  503. {
  504. view?.GetType ().GetProperty ("Title")?.GetSetMethod ()?.Invoke (view, new [] { "Test Title" });
  505. }
  506. }
  507. // If the view supports a Source property, set it so we have something to look at
  508. if (view != null
  509. && view.GetType ().GetProperty ("Source") != null
  510. && view.GetType ().GetProperty ("Source")!.PropertyType == typeof (IListDataSource))
  511. {
  512. ListWrapper<string> source = new (["Test Text #1", "Test Text #2", "Test Text #3"]);
  513. view?.GetType ().GetProperty ("Source")?.GetSetMethod ()?.Invoke (view, new [] { source });
  514. }
  515. // Add
  516. hostPane.Add (view);
  517. //DimPosChanged ();
  518. hostPane.LayoutSubViews ();
  519. hostPane.ClearViewport ();
  520. hostPane.SetNeedsDraw ();
  521. UpdateSettings (view!);
  522. UpdateTitle (view);
  523. view!.SubViewsLaidOut += LayoutCompleteHandler;
  524. return view;
  525. }
  526. void LayoutCompleteHandler (object? sender, LayoutEventArgs args) { UpdateTitle (curView); }
  527. }
  528. }