BeginEndTests.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. using Xunit.Abstractions;
  2. namespace ApplicationTests.BeginEnd;
  3. /// <summary>
  4. /// Comprehensive tests for ApplicationImpl.Begin/End logic that manages Current and SessionStack.
  5. /// These tests ensure the fragile state management logic is robust and catches regressions.
  6. /// Tests work directly with ApplicationImpl instances to avoid global Application state issues.
  7. /// </summary>
  8. public class ApplicationImplBeginEndTests (ITestOutputHelper output)
  9. {
  10. private readonly ITestOutputHelper _output = output;
  11. [Fact]
  12. public void Init_Begin_End_Cleans_Up ()
  13. {
  14. IApplication? app = Application.Create ();
  15. SessionToken? newSessionToken = null;
  16. EventHandler<SessionTokenEventArgs> newSessionTokenFn = (s, e) =>
  17. {
  18. Assert.NotNull (e.State);
  19. newSessionToken = e.State;
  20. };
  21. app.SessionBegun += newSessionTokenFn;
  22. Runnable<bool> runnable = new ();
  23. SessionToken sessionToken = app.Begin (runnable)!;
  24. Assert.NotNull (sessionToken);
  25. Assert.NotNull (newSessionToken);
  26. Assert.Equal (sessionToken, newSessionToken);
  27. // Assert.Equal (runnable, Application.TopRunnable);
  28. app.SessionBegun -= newSessionTokenFn;
  29. app.End (newSessionToken);
  30. Assert.Null (app.TopRunnable);
  31. Assert.Null (app.Driver);
  32. runnable.Dispose ();
  33. }
  34. [Fact]
  35. public void Begin_Null_Runnable_Throws ()
  36. {
  37. IApplication app = Application.Create ();
  38. app.Init ("fake");
  39. // Test null Runnable
  40. Assert.Throws<ArgumentNullException> (() => app.Begin (null!));
  41. app.Dispose ();
  42. }
  43. [Fact]
  44. public void Begin_Sets_Application_Top_To_Console_Size ()
  45. {
  46. IApplication app = Application.Create ();
  47. app.Init ("fake");
  48. Assert.Null (app.TopRunnableView);
  49. app.Driver!.SetScreenSize (80, 25);
  50. Runnable top = new ();
  51. SessionToken? token = app.Begin (top);
  52. Assert.Equal (new (0, 0, 80, 25), app.TopRunnableView!.Frame);
  53. app.Driver!.SetScreenSize (5, 5);
  54. app.LayoutAndDraw ();
  55. Assert.Equal (new (0, 0, 5, 5), app.TopRunnableView!.Frame);
  56. if (token is { })
  57. {
  58. app.End (token);
  59. }
  60. top.Dispose ();
  61. app.Dispose ();
  62. }
  63. [Fact]
  64. public void Begin_WithNullRunnable_ThrowsArgumentNullException ()
  65. {
  66. IApplication app = Application.Create ();
  67. try
  68. {
  69. Assert.Throws<ArgumentNullException> (() => app.Begin (null!));
  70. }
  71. finally
  72. {
  73. app.Dispose ();
  74. }
  75. }
  76. [Fact]
  77. public void Begin_SetsCurrent_WhenCurrentIsNull ()
  78. {
  79. IApplication app = Application.Create ();
  80. Runnable? runnable = null;
  81. try
  82. {
  83. runnable = new ();
  84. Assert.Null (app.TopRunnableView);
  85. app.Begin (runnable);
  86. Assert.NotNull (app.TopRunnableView);
  87. Assert.Same (runnable, app.TopRunnableView);
  88. Assert.Single (app.SessionStack!);
  89. }
  90. finally
  91. {
  92. runnable?.Dispose ();
  93. app.Dispose ();
  94. }
  95. }
  96. [Fact]
  97. public void Begin_PushesToSessionStack ()
  98. {
  99. IApplication app = Application.Create ();
  100. Runnable? runnable1 = null;
  101. Runnable? runnable2 = null;
  102. try
  103. {
  104. runnable1 = new () { Id = "1" };
  105. runnable2 = new () { Id = "2" };
  106. app.Begin (runnable1);
  107. Assert.Single (app.SessionStack!);
  108. Assert.Same (runnable1, app.TopRunnableView);
  109. app.Begin (runnable2);
  110. Assert.Equal (2, app.SessionStack!.Count);
  111. Assert.Same (runnable2, app.TopRunnableView);
  112. }
  113. finally
  114. {
  115. runnable1?.Dispose ();
  116. runnable2?.Dispose ();
  117. app.Dispose ();
  118. }
  119. }
  120. [Fact]
  121. public void End_WithNullSessionToken_ThrowsArgumentNullException ()
  122. {
  123. IApplication app = Application.Create ();
  124. try
  125. {
  126. Assert.Throws<ArgumentNullException> (() => app.End (null!));
  127. }
  128. finally
  129. {
  130. app.Dispose ();
  131. }
  132. }
  133. [Fact]
  134. public void End_PopsSessionStack ()
  135. {
  136. IApplication app = Application.Create ();
  137. Runnable? runnable1 = null;
  138. Runnable? runnable2 = null;
  139. try
  140. {
  141. runnable1 = new () { Id = "1" };
  142. runnable2 = new () { Id = "2" };
  143. SessionToken token1 = app.Begin (runnable1)!;
  144. SessionToken token2 = app.Begin (runnable2)!;
  145. Assert.Equal (2, app.SessionStack!.Count);
  146. app.End (token2);
  147. Assert.Single (app.SessionStack!);
  148. Assert.Same (runnable1, app.TopRunnableView);
  149. app.End (token1);
  150. Assert.Empty (app.SessionStack!);
  151. }
  152. finally
  153. {
  154. runnable1?.Dispose ();
  155. runnable2?.Dispose ();
  156. app.Dispose ();
  157. }
  158. }
  159. [Fact (Skip = "This test may be bogus. What's wrong with ending a non-top session?")]
  160. public void End_ThrowsArgumentException_WhenNotBalanced ()
  161. {
  162. IApplication app = Application.Create ();
  163. Runnable? runnable1 = null;
  164. Runnable? runnable2 = null;
  165. try
  166. {
  167. runnable1 = new () { Id = "1" };
  168. runnable2 = new () { Id = "2" };
  169. SessionToken? token1 = app.Begin (runnable1);
  170. SessionToken? token2 = app.Begin (runnable2);
  171. // Trying to end token1 when token2 is on top should throw
  172. // NOTE: This throws but has the side effect of popping token2 from the stack
  173. Assert.Throws<ArgumentException> (() => app.End (token1!));
  174. // Don't try to clean up with more End calls - the state is now inconsistent
  175. // Let Shutdown/ResetState handle cleanup
  176. }
  177. finally
  178. {
  179. // Dispose runnables BEFORE Shutdown to satisfy DEBUG_IDISPOSABLE assertions
  180. runnable1?.Dispose ();
  181. runnable2?.Dispose ();
  182. // Shutdown will call ResetState which clears any remaining state
  183. app.Dispose ();
  184. }
  185. }
  186. [Fact]
  187. public void End_RestoresCurrentToPreviousRunnable ()
  188. {
  189. IApplication app = Application.Create ();
  190. Runnable? runnable1 = null;
  191. Runnable? runnable2 = null;
  192. Runnable? runnable3 = null;
  193. try
  194. {
  195. runnable1 = new () { Id = "1" };
  196. runnable2 = new () { Id = "2" };
  197. runnable3 = new () { Id = "3" };
  198. SessionToken? token1 = app.Begin (runnable1);
  199. SessionToken? token2 = app.Begin (runnable2);
  200. SessionToken? token3 = app.Begin (runnable3);
  201. Assert.Same (runnable3, app.TopRunnableView);
  202. app.End (token3!);
  203. Assert.Same (runnable2, app.TopRunnableView);
  204. app.End (token2!);
  205. Assert.Same (runnable1, app.TopRunnableView);
  206. app.End (token1!);
  207. }
  208. finally
  209. {
  210. runnable1?.Dispose ();
  211. runnable2?.Dispose ();
  212. runnable3?.Dispose ();
  213. app.Dispose ();
  214. }
  215. }
  216. [Fact]
  217. public void MultipleBeginEnd_MaintainsStackIntegrity ()
  218. {
  219. IApplication app = Application.Create ();
  220. List<Runnable> runnables = new ();
  221. List<SessionToken> tokens = new ();
  222. try
  223. {
  224. // Begin multiple runnables
  225. for (var i = 0; i < 5; i++)
  226. {
  227. var runnable = new Runnable { Id = $"runnable-{i}" };
  228. runnables.Add (runnable);
  229. SessionToken? token = app.Begin (runnable);
  230. tokens.Add (token!);
  231. }
  232. Assert.Equal (5, app.SessionStack!.Count);
  233. Assert.Same (runnables [4], app.TopRunnableView);
  234. // End them in reverse order (LIFO)
  235. for (var i = 4; i >= 0; i--)
  236. {
  237. app.End (tokens [i]);
  238. if (i > 0)
  239. {
  240. Assert.Equal (i, app.SessionStack.Count);
  241. Assert.Same (runnables [i - 1], app.TopRunnableView);
  242. }
  243. else
  244. {
  245. Assert.Empty (app.SessionStack);
  246. }
  247. }
  248. }
  249. finally
  250. {
  251. foreach (Runnable runnable in runnables)
  252. {
  253. runnable.Dispose ();
  254. }
  255. app.Dispose ();
  256. }
  257. }
  258. [Fact]
  259. public void End_NullsSessionTokenRunnable ()
  260. {
  261. IApplication app = Application.Create ();
  262. Runnable? runnable = null;
  263. try
  264. {
  265. runnable = new ();
  266. SessionToken? token = app.Begin (runnable);
  267. Assert.Same (runnable, token!.Runnable);
  268. app.End (token);
  269. Assert.Null (token.Runnable);
  270. }
  271. finally
  272. {
  273. runnable?.Dispose ();
  274. app.Dispose ();
  275. }
  276. }
  277. [Fact]
  278. public void ResetState_ClearsSessionStack ()
  279. {
  280. IApplication app = Application.Create ();
  281. Runnable? runnable1 = null;
  282. Runnable? runnable2 = null;
  283. try
  284. {
  285. runnable1 = new () { Id = "1" };
  286. runnable2 = new () { Id = "2" };
  287. app.Begin (runnable1);
  288. app.Begin (runnable2);
  289. Assert.Equal (2, app.SessionStack!.Count);
  290. Assert.NotNull (app.TopRunnableView);
  291. }
  292. finally
  293. {
  294. // Dispose runnables BEFORE Shutdown to satisfy DEBUG_IDISPOSABLE assertions
  295. runnable1?.Dispose ();
  296. runnable2?.Dispose ();
  297. // Shutdown calls ResetState, which will clear SessionStack and set Current to null
  298. app.Dispose ();
  299. // Verify cleanup happened
  300. Assert.Empty (app.SessionStack!);
  301. Assert.Null (app.TopRunnableView);
  302. }
  303. }
  304. [Fact]
  305. public void ResetState_StopsAllRunningRunnables ()
  306. {
  307. IApplication app = Application.Create ();
  308. Runnable? runnable1 = null;
  309. Runnable? runnable2 = null;
  310. try
  311. {
  312. runnable1 = new () { Id = "1" };
  313. runnable2 = new () { Id = "2" };
  314. app.Begin (runnable1);
  315. app.Begin (runnable2);
  316. Assert.True (runnable1.IsRunning);
  317. Assert.True (runnable2.IsRunning);
  318. }
  319. finally
  320. {
  321. // Dispose runnables BEFORE Shutdown to satisfy DEBUG_IDISPOSABLE assertions
  322. runnable1?.Dispose ();
  323. runnable2?.Dispose ();
  324. // Shutdown calls ResetState, which will stop all running runnables
  325. app.Dispose ();
  326. // Verify runnables were stopped
  327. Assert.False (runnable1!.IsRunning);
  328. Assert.False (runnable2!.IsRunning);
  329. }
  330. }
  331. //[Fact]
  332. //public void Begin_ActivatesNewRunnable_WhenCurrentExists ()
  333. //{
  334. // IApplication app = Application.Create ();
  335. // Runnable? runnable1 = null;
  336. // Runnable? runnable2 = null;
  337. // try
  338. // {
  339. // runnable1 = new () { Id = "1" };
  340. // runnable2 = new () { Id = "2" };
  341. // var runnable1Deactivated = false;
  342. // var runnable2Activated = false;
  343. // runnable1.Deactivate += (s, e) => runnable1Deactivated = true;
  344. // runnable2.Activate += (s, e) => runnable2Activated = true;
  345. // app.Begin (runnable1);
  346. // app.Begin (runnable2);
  347. // Assert.True (runnable1Deactivated);
  348. // Assert.True (runnable2Activated);
  349. // Assert.Same (runnable2, app.TopRunnable);
  350. // }
  351. // finally
  352. // {
  353. // runnable1?.Dispose ();
  354. // runnable2?.Dispose ();
  355. // app.Dispose ();
  356. // }
  357. //}
  358. [Fact]
  359. public void SessionStack_ContainsAllBegunRunnables ()
  360. {
  361. IApplication app = Application.Create ();
  362. List<Runnable> runnables = new ();
  363. try
  364. {
  365. for (var i = 0; i < 10; i++)
  366. {
  367. var runnable = new Runnable { Id = $"runnable-{i}" };
  368. runnables.Add (runnable);
  369. app.Begin (runnable);
  370. }
  371. // All runnables should be in the stack
  372. Assert.Equal (10, app.SessionStack!.Count);
  373. // Verify stack contains all runnables
  374. List<SessionToken> stackList = app.SessionStack.ToList ();
  375. foreach (Runnable runnable in runnables)
  376. {
  377. Assert.Contains (runnable, stackList.Select (r => r.Runnable));
  378. }
  379. }
  380. finally
  381. {
  382. foreach (Runnable runnable in runnables)
  383. {
  384. runnable.Dispose ();
  385. }
  386. app.Dispose ();
  387. }
  388. }
  389. }