ApplicationImplBeginEndTests.cs 11 KB

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