ApplicationV2Tests.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. #nullable enable
  2. using System.Collections.Concurrent;
  3. using System.Runtime.CompilerServices;
  4. using Microsoft.Extensions.Logging;
  5. using Moq;
  6. namespace UnitTests.ConsoleDrivers.V2;
  7. public class ApplicationV2Tests
  8. {
  9. public ApplicationV2Tests ()
  10. {
  11. ConsoleDriver.RunningUnitTests = true;
  12. }
  13. private ApplicationV2 NewApplicationV2 ()
  14. {
  15. var netInput = new Mock<INetInput> ();
  16. SetupRunInputMockMethodToBlock (netInput);
  17. var winInput = new Mock<IWindowsInput> ();
  18. SetupRunInputMockMethodToBlock (winInput);
  19. return new (
  20. () => netInput.Object,
  21. Mock.Of<IConsoleOutput>,
  22. () => winInput.Object,
  23. Mock.Of<IConsoleOutput>);
  24. }
  25. [Fact]
  26. public void Init_CreatesKeybindings ()
  27. {
  28. var orig = ApplicationImpl.Instance;
  29. var v2 = NewApplicationV2 ();
  30. ApplicationImpl.ChangeInstance (v2);
  31. Application.KeyBindings.Clear ();
  32. Assert.Empty (Application.KeyBindings.GetBindings ());
  33. v2.Init ();
  34. Assert.NotEmpty (Application.KeyBindings.GetBindings ());
  35. v2.Shutdown ();
  36. ApplicationImpl.ChangeInstance (orig);
  37. }
  38. [Fact]
  39. public void Init_DriverIsFacade ()
  40. {
  41. var orig = ApplicationImpl.Instance;
  42. var v2 = NewApplicationV2 ();
  43. ApplicationImpl.ChangeInstance (v2);
  44. Assert.Null (Application.Driver);
  45. v2.Init ();
  46. Assert.NotNull (Application.Driver);
  47. var type = Application.Driver.GetType ();
  48. Assert.True (type.IsGenericType);
  49. Assert.True (type.GetGenericTypeDefinition () == typeof (ConsoleDriverFacade<>));
  50. v2.Shutdown ();
  51. Assert.Null (Application.Driver);
  52. ApplicationImpl.ChangeInstance (orig);
  53. }
  54. [Fact]
  55. public void Init_ExplicitlyRequestWin ()
  56. {
  57. var orig = ApplicationImpl.Instance;
  58. Assert.Null (Application.Driver);
  59. var netInput = new Mock<INetInput> (MockBehavior.Strict);
  60. var netOutput = new Mock<IConsoleOutput> (MockBehavior.Strict);
  61. var winInput = new Mock<IWindowsInput> (MockBehavior.Strict);
  62. var winOutput = new Mock<IConsoleOutput> (MockBehavior.Strict);
  63. winInput.Setup (i => i.Initialize (It.IsAny<ConcurrentQueue<WindowsConsole.InputRecord>> ()))
  64. .Verifiable (Times.Once);
  65. SetupRunInputMockMethodToBlock (winInput);
  66. winInput.Setup (i => i.Dispose ())
  67. .Verifiable (Times.Once);
  68. winOutput.Setup (i => i.Dispose ())
  69. .Verifiable (Times.Once);
  70. var v2 = new ApplicationV2 (
  71. () => netInput.Object,
  72. () => netOutput.Object,
  73. () => winInput.Object,
  74. () => winOutput.Object);
  75. ApplicationImpl.ChangeInstance (v2);
  76. Assert.Null (Application.Driver);
  77. v2.Init (null, "v2win");
  78. Assert.NotNull (Application.Driver);
  79. var type = Application.Driver.GetType ();
  80. Assert.True (type.IsGenericType);
  81. Assert.True (type.GetGenericTypeDefinition () == typeof (ConsoleDriverFacade<>));
  82. v2.Shutdown ();
  83. Assert.Null (Application.Driver);
  84. winInput.VerifyAll ();
  85. ApplicationImpl.ChangeInstance (orig);
  86. }
  87. [Fact]
  88. public void Init_ExplicitlyRequestNet ()
  89. {
  90. var orig = ApplicationImpl.Instance;
  91. var netInput = new Mock<INetInput> (MockBehavior.Strict);
  92. var netOutput = new Mock<IConsoleOutput> (MockBehavior.Strict);
  93. var winInput = new Mock<IWindowsInput> (MockBehavior.Strict);
  94. var winOutput = new Mock<IConsoleOutput> (MockBehavior.Strict);
  95. netInput.Setup (i => i.Initialize (It.IsAny<ConcurrentQueue<ConsoleKeyInfo>> ()))
  96. .Verifiable (Times.Once);
  97. SetupRunInputMockMethodToBlock (netInput);
  98. netInput.Setup (i => i.Dispose ())
  99. .Verifiable (Times.Once);
  100. netOutput.Setup (i => i.Dispose ())
  101. .Verifiable (Times.Once);
  102. var v2 = new ApplicationV2 (
  103. () => netInput.Object,
  104. () => netOutput.Object,
  105. () => winInput.Object,
  106. () => winOutput.Object);
  107. ApplicationImpl.ChangeInstance (v2);
  108. Assert.Null (Application.Driver);
  109. v2.Init (null, "v2net");
  110. Assert.NotNull (Application.Driver);
  111. var type = Application.Driver.GetType ();
  112. Assert.True (type.IsGenericType);
  113. Assert.True (type.GetGenericTypeDefinition () == typeof (ConsoleDriverFacade<>));
  114. v2.Shutdown ();
  115. Assert.Null (Application.Driver);
  116. netInput.VerifyAll ();
  117. ApplicationImpl.ChangeInstance (orig);
  118. }
  119. private void SetupRunInputMockMethodToBlock (Mock<IWindowsInput> winInput)
  120. {
  121. winInput.Setup (r => r.Run (It.IsAny<CancellationToken> ()))
  122. .Callback<CancellationToken> (token =>
  123. {
  124. // Simulate an infinite loop that checks for cancellation
  125. while (!token.IsCancellationRequested)
  126. {
  127. // Perform the action that should repeat in the loop
  128. // This could be some mock behavior or just an empty loop depending on the context
  129. }
  130. })
  131. .Verifiable (Times.Once);
  132. }
  133. private void SetupRunInputMockMethodToBlock (Mock<INetInput> netInput)
  134. {
  135. netInput.Setup (r => r.Run (It.IsAny<CancellationToken> ()))
  136. .Callback<CancellationToken> (token =>
  137. {
  138. // Simulate an infinite loop that checks for cancellation
  139. while (!token.IsCancellationRequested)
  140. {
  141. // Perform the action that should repeat in the loop
  142. // This could be some mock behavior or just an empty loop depending on the context
  143. }
  144. })
  145. .Verifiable (Times.Once);
  146. }
  147. [Fact]
  148. public void NoInitThrowOnRun ()
  149. {
  150. var orig = ApplicationImpl.Instance;
  151. Assert.Null (Application.Driver);
  152. var app = NewApplicationV2 ();
  153. ApplicationImpl.ChangeInstance (app);
  154. var ex = Assert.Throws<NotInitializedException> (() => app.Run (new Window ()));
  155. Assert.Equal ("Run cannot be accessed before Initialization", ex.Message);
  156. app.Shutdown();
  157. ApplicationImpl.ChangeInstance (orig);
  158. }
  159. [Fact]
  160. public void InitRunShutdown_Top_Set_To_Null_After_Shutdown ()
  161. {
  162. var orig = ApplicationImpl.Instance;
  163. var v2 = NewApplicationV2 ();
  164. ApplicationImpl.ChangeInstance (v2);
  165. v2.Init ();
  166. var timeoutToken = v2.AddTimeout (TimeSpan.FromMilliseconds (150),
  167. () =>
  168. {
  169. if (Application.Top != null)
  170. {
  171. Application.RequestStop ();
  172. return false;
  173. }
  174. return false;
  175. }
  176. );
  177. Assert.Null (Application.Top);
  178. // Blocks until the timeout call is hit
  179. v2.Run (new Window ());
  180. // We returned false above, so we should not have to remove the timeout
  181. Assert.False(v2.RemoveTimeout (timeoutToken));
  182. Assert.NotNull (Application.Top);
  183. Application.Top?.Dispose ();
  184. v2.Shutdown ();
  185. Assert.Null (Application.Top);
  186. ApplicationImpl.ChangeInstance (orig);
  187. }
  188. [Fact]
  189. public void InitRunShutdown_Running_Set_To_False ()
  190. {
  191. var orig = ApplicationImpl.Instance;
  192. var v2 = NewApplicationV2 ();
  193. ApplicationImpl.ChangeInstance (v2);
  194. v2.Init ();
  195. Toplevel top = new Window ()
  196. {
  197. Title = "InitRunShutdown_Running_Set_To_False"
  198. };
  199. var timeoutToken = v2.AddTimeout (TimeSpan.FromMilliseconds (150),
  200. () =>
  201. {
  202. Assert.True (top!.Running);
  203. if (Application.Top != null)
  204. {
  205. Application.RequestStop ();
  206. return false;
  207. }
  208. return false;
  209. }
  210. );
  211. Assert.False (top!.Running);
  212. // Blocks until the timeout call is hit
  213. v2.Run (top);
  214. // We returned false above, so we should not have to remove the timeout
  215. Assert.False (v2.RemoveTimeout (timeoutToken));
  216. Assert.False (top!.Running);
  217. // BUGBUG: Shutdown sets Top to null, not End.
  218. //Assert.Null (Application.Top);
  219. Application.Top?.Dispose ();
  220. v2.Shutdown ();
  221. ApplicationImpl.ChangeInstance (orig);
  222. }
  223. [Fact]
  224. public void InitRunShutdown_End_Is_Called ()
  225. {
  226. var orig = ApplicationImpl.Instance;
  227. var v2 = NewApplicationV2 ();
  228. ApplicationImpl.ChangeInstance (v2);
  229. Assert.Null (Application.Top);
  230. Assert.Null (Application.Driver);
  231. v2.Init ();
  232. Toplevel top = new Window ();
  233. // BUGBUG: Both Closed and Unloaded are called from End; what's the difference?
  234. int closedCount = 0;
  235. top.Closed
  236. += (_, a) =>
  237. {
  238. closedCount++;
  239. };
  240. int unloadedCount = 0;
  241. top.Unloaded
  242. += (_, a) =>
  243. {
  244. unloadedCount++;
  245. };
  246. var timeoutToken = v2.AddTimeout (TimeSpan.FromMilliseconds (150),
  247. () =>
  248. {
  249. Assert.True (top!.Running);
  250. if (Application.Top != null)
  251. {
  252. Application.RequestStop ();
  253. return false;
  254. }
  255. return false;
  256. }
  257. );
  258. Assert.Equal (0, closedCount);
  259. Assert.Equal (0, unloadedCount);
  260. // Blocks until the timeout call is hit
  261. v2.Run (top);
  262. Assert.Equal (1, closedCount);
  263. Assert.Equal (1, unloadedCount);
  264. // We returned false above, so we should not have to remove the timeout
  265. Assert.False (v2.RemoveTimeout (timeoutToken));
  266. Application.Top?.Dispose ();
  267. v2.Shutdown ();
  268. Assert.Equal (1, closedCount);
  269. Assert.Equal (1, unloadedCount);
  270. ApplicationImpl.ChangeInstance (orig);
  271. }
  272. [Fact]
  273. public void InitRunShutdown_QuitKey_Quits ()
  274. {
  275. var orig = ApplicationImpl.Instance;
  276. var v2 = NewApplicationV2 ();
  277. ApplicationImpl.ChangeInstance (v2);
  278. v2.Init ();
  279. Toplevel top = new Window ()
  280. {
  281. Title = "InitRunShutdown_QuitKey_Quits"
  282. };
  283. var timeoutToken = v2.AddTimeout (TimeSpan.FromMilliseconds (150),
  284. () =>
  285. {
  286. Assert.True (top!.Running);
  287. if (Application.Top != null)
  288. {
  289. Application.RaiseKeyDownEvent (Application.QuitKey);
  290. }
  291. return false;
  292. }
  293. );
  294. Assert.False (top!.Running);
  295. // Blocks until the timeout call is hit
  296. v2.Run (top);
  297. // We returned false above, so we should not have to remove the timeout
  298. Assert.False (v2.RemoveTimeout (timeoutToken));
  299. Assert.False (top!.Running);
  300. Assert.NotNull (Application.Top);
  301. top.Dispose ();
  302. v2.Shutdown ();
  303. Assert.Null (Application.Top);
  304. ApplicationImpl.ChangeInstance (orig);
  305. }
  306. [Fact]
  307. public void InitRunShutdown_Generic_IdleForExit ()
  308. {
  309. var orig = ApplicationImpl.Instance;
  310. var v2 = NewApplicationV2 ();
  311. ApplicationImpl.ChangeInstance (v2);
  312. v2.Init ();
  313. v2.AddIdle (IdleExit);
  314. Assert.Null (Application.Top);
  315. // Blocks until the timeout call is hit
  316. v2.Run<Window> ();
  317. Assert.NotNull (Application.Top);
  318. Application.Top?.Dispose ();
  319. v2.Shutdown ();
  320. Assert.Null (Application.Top);
  321. ApplicationImpl.ChangeInstance (orig);
  322. }
  323. [Fact]
  324. public void Shutdown_Closing_Closed_Raised ()
  325. {
  326. var orig = ApplicationImpl.Instance;
  327. var v2 = NewApplicationV2 ();
  328. ApplicationImpl.ChangeInstance (v2);
  329. v2.Init ();
  330. int closing = 0;
  331. int closed = 0;
  332. var t = new Toplevel ();
  333. t.Closing
  334. += (_, a) =>
  335. {
  336. // Cancel the first time
  337. if (closing == 0)
  338. {
  339. a.Cancel = true;
  340. }
  341. closing++;
  342. Assert.Same (t, a.RequestingTop);
  343. };
  344. t.Closed
  345. += (_, a) =>
  346. {
  347. closed++;
  348. Assert.Same (t, a.Toplevel);
  349. };
  350. v2.AddIdle (IdleExit);
  351. // Blocks until the timeout call is hit
  352. v2.Run (t);
  353. Application.Top?.Dispose ();
  354. v2.Shutdown ();
  355. ApplicationImpl.ChangeInstance (orig);
  356. Assert.Equal (2, closing);
  357. Assert.Equal (1, closed);
  358. }
  359. private bool IdleExit ()
  360. {
  361. if (Application.Top != null)
  362. {
  363. Application.RequestStop ();
  364. return true;
  365. }
  366. return true;
  367. }
  368. [Fact]
  369. public void Shutdown_Called_Repeatedly_DoNotDuplicateDisposeOutput ()
  370. {
  371. var orig = ApplicationImpl.Instance;
  372. var netInput = new Mock<INetInput> ();
  373. SetupRunInputMockMethodToBlock (netInput);
  374. Mock<IConsoleOutput>? outputMock = null;
  375. var v2 = new ApplicationV2 (
  376. () => netInput.Object,
  377. () => (outputMock = new Mock<IConsoleOutput> ()).Object,
  378. Mock.Of<IWindowsInput>,
  379. Mock.Of<IConsoleOutput>);
  380. ApplicationImpl.ChangeInstance (v2);
  381. v2.Init (null, "v2net");
  382. v2.Shutdown ();
  383. v2.Shutdown ();
  384. outputMock!.Verify (o => o.Dispose (), Times.Once);
  385. ApplicationImpl.ChangeInstance (orig);
  386. }
  387. [Fact]
  388. public void Init_Called_Repeatedly_WarnsAndIgnores ()
  389. {
  390. var orig = ApplicationImpl.Instance;
  391. var v2 = NewApplicationV2 ();
  392. ApplicationImpl.ChangeInstance (v2);
  393. Assert.Null (Application.Driver);
  394. v2.Init ();
  395. Assert.NotNull (Application.Driver);
  396. var mockLogger = new Mock<ILogger> ();
  397. var beforeLogger = Logging.Logger;
  398. Logging.Logger = mockLogger.Object;
  399. v2.Init ();
  400. v2.Init ();
  401. mockLogger.Verify (
  402. l => l.Log (LogLevel.Error,
  403. It.IsAny<EventId> (),
  404. It.Is<It.IsAnyType> ((v, t) => v.ToString () == "Init called multiple times without shutdown, ignoring."),
  405. It.IsAny<Exception> (),
  406. It.IsAny<Func<It.IsAnyType, Exception, string>> ()!)
  407. , Times.Exactly (2));
  408. v2.Shutdown ();
  409. // Restore the original null logger to be polite to other tests
  410. Logging.Logger = beforeLogger;
  411. ApplicationImpl.ChangeInstance (orig);
  412. }
  413. [Fact]
  414. public void Open_Calls_ContinueWith_On_UIThread ()
  415. {
  416. var orig = ApplicationImpl.Instance;
  417. var v2 = NewApplicationV2 ();
  418. ApplicationImpl.ChangeInstance (v2);
  419. v2.Init ();
  420. var b = new Button ();
  421. bool result = false;
  422. b.Accepting +=
  423. (_, _) =>
  424. {
  425. Task.Run (() =>
  426. {
  427. Task.Delay (300).Wait ();
  428. }).ContinueWith (
  429. (t, _) =>
  430. {
  431. // no longer loading
  432. Application.Invoke (() =>
  433. {
  434. result = true;
  435. Application.RequestStop ();
  436. });
  437. },
  438. TaskScheduler.FromCurrentSynchronizationContext ());
  439. };
  440. v2.AddTimeout (TimeSpan.FromMilliseconds (150),
  441. () =>
  442. {
  443. // Run asynchronous logic inside Task.Run
  444. if (Application.Top != null)
  445. {
  446. b.NewKeyDownEvent (Key.Enter);
  447. b.NewKeyUpEvent (Key.Enter);
  448. }
  449. return false;
  450. });
  451. Assert.Null (Application.Top);
  452. var w = new Window ()
  453. {
  454. Title = "Open_CallsContinueWithOnUIThread"
  455. };
  456. w.Add (b);
  457. // Blocks until the timeout call is hit
  458. v2.Run (w);
  459. Assert.NotNull (Application.Top);
  460. Application.Top?.Dispose ();
  461. v2.Shutdown ();
  462. Assert.Null (Application.Top);
  463. ApplicationImpl.ChangeInstance (orig);
  464. Assert.True (result);
  465. }
  466. }