TimeoutTests.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856
  1. using Xunit.Abstractions;
  2. // ReSharper disable AccessToDisposedClosure
  3. #pragma warning disable xUnit1031
  4. namespace ApplicationTests.Timeout;
  5. /// <summary>
  6. /// Tests for timeout behavior and functionality.
  7. /// These tests verify that timeouts fire correctly, can be added/removed,
  8. /// handle exceptions properly, and work with Application.Run() calls.
  9. /// </summary>
  10. public class TimeoutTests (ITestOutputHelper output)
  11. {
  12. [Fact]
  13. public void AddTimeout_Callback_Can_Add_New_Timeout ()
  14. {
  15. using IApplication app = Application.Create ();
  16. app.Init ("fake");
  17. var firstFired = false;
  18. var secondFired = false;
  19. app.AddTimeout (
  20. TimeSpan.FromMilliseconds (50),
  21. () =>
  22. {
  23. firstFired = true;
  24. // Add another timeout from within callback
  25. app.AddTimeout (
  26. TimeSpan.FromMilliseconds (50),
  27. () =>
  28. {
  29. secondFired = true;
  30. app.RequestStop ();
  31. return false;
  32. }
  33. );
  34. return false;
  35. }
  36. );
  37. // Defensive: use iteration counter instead of time-based safety timeout
  38. var iterations = 0;
  39. app.Iteration += IterationHandler;
  40. try
  41. {
  42. app.Run<Runnable> ();
  43. Assert.True (firstFired);
  44. Assert.True (secondFired);
  45. }
  46. finally
  47. {
  48. app.Iteration -= IterationHandler;
  49. }
  50. return;
  51. void IterationHandler (object? s, EventArgs<IApplication?> e)
  52. {
  53. iterations++;
  54. // Stop if test objectives met or safety limit reached
  55. if ((firstFired && secondFired) || iterations > 1000)
  56. {
  57. app.RequestStop ();
  58. }
  59. }
  60. }
  61. [Fact]
  62. public void AddTimeout_Exception_In_Callback_Propagates ()
  63. {
  64. using IApplication app = Application.Create ();
  65. app.Init ("fake");
  66. var exceptionThrown = false;
  67. app.AddTimeout (
  68. TimeSpan.FromMilliseconds (50),
  69. () =>
  70. {
  71. exceptionThrown = true;
  72. throw new InvalidOperationException ("Test exception");
  73. });
  74. // Defensive: use iteration counter
  75. var iterations = 0;
  76. app.Iteration += IterationHandler;
  77. try
  78. {
  79. Assert.Throws<InvalidOperationException> (() => app.Run<Runnable> ());
  80. Assert.True (exceptionThrown, "Exception callback should have been invoked");
  81. }
  82. finally
  83. {
  84. app.Iteration -= IterationHandler;
  85. }
  86. return;
  87. void IterationHandler (object? s, EventArgs<IApplication?> e)
  88. {
  89. iterations++;
  90. // Safety stop if exception not thrown after many iterations
  91. if (iterations > 1000 && !exceptionThrown)
  92. {
  93. app.RequestStop ();
  94. }
  95. }
  96. }
  97. [Fact]
  98. public void AddTimeout_Fires ()
  99. {
  100. using IApplication app = Application.Create ();
  101. app.Init ("fake");
  102. uint timeoutTime = 100;
  103. var timeoutFired = false;
  104. // Setup a timeout that will fire
  105. app.AddTimeout (
  106. TimeSpan.FromMilliseconds (timeoutTime),
  107. () =>
  108. {
  109. timeoutFired = true;
  110. // Return false so the timer does not repeat
  111. return false;
  112. }
  113. );
  114. // The timeout has not fired yet
  115. Assert.False (timeoutFired);
  116. // Block the thread to prove the timeout does not fire on a background thread
  117. Thread.Sleep ((int)timeoutTime * 2);
  118. Assert.False (timeoutFired);
  119. app.StopAfterFirstIteration = true;
  120. app.Run<Runnable> ();
  121. // The timeout should have fired
  122. Assert.True (timeoutFired);
  123. }
  124. [Fact]
  125. public void AddTimeout_From_Background_Thread_Fires ()
  126. {
  127. using IApplication app = Application.Create ();
  128. app.Init ("fake");
  129. var timeoutFired = false;
  130. using var taskCompleted = new ManualResetEventSlim (false);
  131. Task.Run (() =>
  132. {
  133. Thread.Sleep (50); // Ensure we're on background thread
  134. app.Invoke (() =>
  135. {
  136. app.AddTimeout (
  137. TimeSpan.FromMilliseconds (100),
  138. () =>
  139. {
  140. timeoutFired = true;
  141. taskCompleted.Set ();
  142. app.RequestStop ();
  143. return false;
  144. }
  145. );
  146. }
  147. );
  148. }
  149. );
  150. // Use iteration counter for safety instead of time
  151. var iterations = 0;
  152. app.Iteration += IterationHandler;
  153. try
  154. {
  155. app.Run<Runnable> ();
  156. // Defensive: wait with timeout
  157. Assert.True (taskCompleted.Wait (TimeSpan.FromSeconds (5)), "Timeout from background thread should have completed");
  158. Assert.True (timeoutFired);
  159. }
  160. finally
  161. {
  162. app.Iteration -= IterationHandler;
  163. }
  164. return;
  165. void IterationHandler (object? s, EventArgs<IApplication?> e)
  166. {
  167. iterations++;
  168. // Safety stop
  169. if (iterations > 1000)
  170. {
  171. app.RequestStop ();
  172. }
  173. }
  174. }
  175. [Fact]
  176. public void AddTimeout_High_Frequency_All_Fire ()
  177. {
  178. using IApplication app = Application.Create ();
  179. app.Init ("fake");
  180. const int TIMEOUT_COUNT = 50; // Reduced from 100 for performance
  181. var firedCount = 0;
  182. for (var i = 0; i < TIMEOUT_COUNT; i++)
  183. {
  184. app.AddTimeout (
  185. TimeSpan.FromMilliseconds (10 + i * 5),
  186. () =>
  187. {
  188. Interlocked.Increment (ref firedCount);
  189. return false;
  190. }
  191. );
  192. }
  193. // Use iteration counter and event completion instead of time-based safety
  194. var iterations = 0;
  195. app.Iteration += IterationHandler;
  196. try
  197. {
  198. app.Run<Runnable> ();
  199. Assert.Equal (TIMEOUT_COUNT, firedCount);
  200. }
  201. finally
  202. {
  203. app.Iteration -= IterationHandler;
  204. }
  205. return;
  206. void IterationHandler (object? s, EventArgs<IApplication?> e)
  207. {
  208. iterations++;
  209. // Stop when all timeouts fired or safety limit reached
  210. if (firedCount >= TIMEOUT_COUNT || iterations > 2000)
  211. {
  212. app.RequestStop ();
  213. }
  214. }
  215. }
  216. [Fact]
  217. public void Long_Running_Callback_Delays_Subsequent_Timeouts ()
  218. {
  219. using IApplication app = Application.Create ();
  220. app.Init ("fake");
  221. var firstStarted = false;
  222. var secondFired = false;
  223. var firstCompleted = false;
  224. // Long-running timeout
  225. app.AddTimeout (
  226. TimeSpan.FromMilliseconds (50),
  227. () =>
  228. {
  229. firstStarted = true;
  230. Thread.Sleep (200); // Simulate long operation
  231. firstCompleted = true;
  232. return false;
  233. }
  234. );
  235. // This should fire even though first is still running
  236. app.AddTimeout (
  237. TimeSpan.FromMilliseconds (100),
  238. () =>
  239. {
  240. secondFired = true;
  241. return false;
  242. }
  243. );
  244. // Use iteration counter instead of time-based timeout
  245. var iterations = 0;
  246. app.Iteration += IterationHandler;
  247. try
  248. {
  249. app.Run<Runnable> ();
  250. Assert.True (firstStarted);
  251. Assert.True (secondFired);
  252. Assert.True (firstCompleted);
  253. }
  254. finally
  255. {
  256. app.Iteration -= IterationHandler;
  257. }
  258. return;
  259. void IterationHandler (object? s, EventArgs<IApplication?> e)
  260. {
  261. iterations++;
  262. // Stop when both complete or safety limit
  263. if ((firstCompleted && secondFired) || iterations > 2000)
  264. {
  265. app.RequestStop ();
  266. }
  267. }
  268. }
  269. [Fact]
  270. public void AddTimeout_Multiple_Fire_In_Order ()
  271. {
  272. using IApplication app = Application.Create ();
  273. app.Init ("fake");
  274. List<int> executionOrder = new ();
  275. app.AddTimeout (
  276. TimeSpan.FromMilliseconds (300),
  277. () =>
  278. {
  279. executionOrder.Add (3);
  280. return false;
  281. });
  282. app.AddTimeout (
  283. TimeSpan.FromMilliseconds (100),
  284. () =>
  285. {
  286. executionOrder.Add (1);
  287. return false;
  288. });
  289. app.AddTimeout (
  290. TimeSpan.FromMilliseconds (200),
  291. () =>
  292. {
  293. executionOrder.Add (2);
  294. return false;
  295. });
  296. var iterations = 0;
  297. app.Iteration += IterationHandler;
  298. try
  299. {
  300. app.Run<Runnable> ();
  301. Assert.Equal (new [] { 1, 2, 3 }, executionOrder);
  302. }
  303. finally
  304. {
  305. app.Iteration -= IterationHandler;
  306. }
  307. return;
  308. void IterationHandler (object? s, EventArgs<IApplication?> e)
  309. {
  310. iterations++;
  311. // Stop after timeouts fire or max iterations (defensive)
  312. if (executionOrder.Count == 3 || iterations > 1000)
  313. {
  314. app.RequestStop ();
  315. }
  316. }
  317. }
  318. [Fact]
  319. public void AddTimeout_Multiple_TimeSpan_Zero_All_Fire ()
  320. {
  321. using IApplication app = Application.Create ();
  322. app.Init ("fake");
  323. const int TIMEOUT_COUNT = 10;
  324. var firedCount = 0;
  325. for (var i = 0; i < TIMEOUT_COUNT; i++)
  326. {
  327. app.AddTimeout (
  328. TimeSpan.Zero,
  329. () =>
  330. {
  331. Interlocked.Increment (ref firedCount);
  332. return false;
  333. }
  334. );
  335. }
  336. var iterations = 0;
  337. app.Iteration += IterationHandler;
  338. try
  339. {
  340. app.Run<Runnable> ();
  341. Assert.Equal (TIMEOUT_COUNT, firedCount);
  342. }
  343. finally
  344. {
  345. app.Iteration -= IterationHandler;
  346. }
  347. return;
  348. void IterationHandler (object? s, EventArgs<IApplication?> e)
  349. {
  350. iterations++;
  351. // Defensive: stop after timeouts fire or max iterations
  352. if (firedCount == TIMEOUT_COUNT || iterations > 100)
  353. {
  354. app.RequestStop ();
  355. }
  356. }
  357. }
  358. [Fact]
  359. public void AddTimeout_Nested_Run_Parent_Timeout_Fires ()
  360. {
  361. using IApplication app = Application.Create ();
  362. app.Init ("fake");
  363. var parentTimeoutFired = false;
  364. var childTimeoutFired = false;
  365. var nestedRunCompleted = false;
  366. // Parent timeout - fires after child modal opens
  367. app.AddTimeout (
  368. TimeSpan.FromMilliseconds (200),
  369. () =>
  370. {
  371. parentTimeoutFired = true;
  372. return false;
  373. }
  374. );
  375. // After 100ms, open nested modal
  376. app.AddTimeout (
  377. TimeSpan.FromMilliseconds (100),
  378. () =>
  379. {
  380. var childRunnable = new Runnable ();
  381. // Child timeout
  382. app.AddTimeout (
  383. TimeSpan.FromMilliseconds (50),
  384. () =>
  385. {
  386. childTimeoutFired = true;
  387. app.RequestStop (childRunnable);
  388. return false;
  389. }
  390. );
  391. app.Run (childRunnable);
  392. nestedRunCompleted = true;
  393. childRunnable.Dispose ();
  394. return false;
  395. }
  396. );
  397. // Use iteration counter instead of time-based safety
  398. var iterations = 0;
  399. app.Iteration += IterationHandler;
  400. try
  401. {
  402. app.Run<Runnable> ();
  403. Assert.True (childTimeoutFired, "Child timeout should fire during nested Run");
  404. Assert.True (parentTimeoutFired, "Parent timeout should continue firing during nested Run");
  405. Assert.True (nestedRunCompleted, "Nested run should have completed");
  406. }
  407. finally
  408. {
  409. app.Iteration -= IterationHandler;
  410. }
  411. return;
  412. void IterationHandler (object? s, EventArgs<IApplication?> e)
  413. {
  414. iterations++;
  415. // Stop when objectives met or safety limit
  416. if ((parentTimeoutFired && nestedRunCompleted) || iterations > 2000)
  417. {
  418. app.RequestStop ();
  419. }
  420. }
  421. }
  422. [Fact]
  423. public void AddTimeout_Repeating_Fires_Multiple_Times ()
  424. {
  425. using IApplication app = Application.Create ();
  426. app.Init ("fake");
  427. var fireCount = 0;
  428. app.AddTimeout (
  429. TimeSpan.FromMilliseconds (50),
  430. () =>
  431. {
  432. fireCount++;
  433. return fireCount < 3; // Repeat 3 times
  434. }
  435. );
  436. var iterations = 0;
  437. app.Iteration += IterationHandler;
  438. try
  439. {
  440. app.Run<Runnable> ();
  441. Assert.Equal (3, fireCount);
  442. }
  443. finally
  444. {
  445. app.Iteration -= IterationHandler;
  446. }
  447. return;
  448. void IterationHandler (object? s, EventArgs<IApplication?> e)
  449. {
  450. iterations++;
  451. // Stop after 3 fires or max iterations (defensive)
  452. if (fireCount >= 3 || iterations > 1000)
  453. {
  454. app.RequestStop ();
  455. }
  456. }
  457. }
  458. [Fact]
  459. public void AddTimeout_StopAfterFirstIteration_Immediate_Fires ()
  460. {
  461. using IApplication app = Application.Create ();
  462. app.Init ("fake");
  463. var timeoutFired = false;
  464. app.AddTimeout (
  465. TimeSpan.Zero,
  466. () =>
  467. {
  468. timeoutFired = true;
  469. return false;
  470. }
  471. );
  472. app.StopAfterFirstIteration = true;
  473. app.Run<Runnable> ();
  474. Assert.True (timeoutFired);
  475. }
  476. [Fact]
  477. public void AddTimeout_TimeSpan_Zero_Fires ()
  478. {
  479. using IApplication app = Application.Create ();
  480. app.Init ("fake");
  481. var timeoutFired = false;
  482. app.AddTimeout (
  483. TimeSpan.Zero,
  484. () =>
  485. {
  486. timeoutFired = true;
  487. return false;
  488. });
  489. app.StopAfterFirstIteration = true;
  490. app.Run<Runnable> ();
  491. Assert.True (timeoutFired);
  492. }
  493. [Fact]
  494. public void RemoveTimeout_Already_Removed_Returns_False ()
  495. {
  496. using IApplication app = Application.Create ();
  497. app.Init ("fake");
  498. object? token = app.AddTimeout (TimeSpan.FromMilliseconds (100), () => false);
  499. // Remove once
  500. bool removed1 = app.RemoveTimeout (token!);
  501. Assert.True (removed1);
  502. // Try to remove again
  503. bool removed2 = app.RemoveTimeout (token!);
  504. Assert.False (removed2);
  505. }
  506. [Fact]
  507. public void RemoveTimeout_Cancels_Timeout ()
  508. {
  509. using IApplication app = Application.Create ();
  510. app.Init ("fake");
  511. var timeoutFired = false;
  512. object? token = app.AddTimeout (
  513. TimeSpan.FromMilliseconds (100),
  514. () =>
  515. {
  516. timeoutFired = true;
  517. return false;
  518. }
  519. );
  520. // Remove timeout before it fires
  521. bool removed = app.RemoveTimeout (token!);
  522. Assert.True (removed);
  523. // Use iteration counter instead of time-based timeout
  524. var iterations = 0;
  525. app.Iteration += IterationHandler;
  526. try
  527. {
  528. app.Run<Runnable> ();
  529. Assert.False (timeoutFired);
  530. }
  531. finally
  532. {
  533. app.Iteration -= IterationHandler;
  534. }
  535. return;
  536. void IterationHandler (object? s, EventArgs<IApplication?> e)
  537. {
  538. iterations++;
  539. // Since timeout was removed, just need enough iterations to prove it won't fire
  540. // With 100ms timeout, give ~50 iterations which is more than enough
  541. if (iterations > 50)
  542. {
  543. app.RequestStop ();
  544. }
  545. }
  546. }
  547. [Fact]
  548. public void RemoveTimeout_Invalid_Token_Returns_False ()
  549. {
  550. using IApplication app = Application.Create ();
  551. app.Init ("fake");
  552. var fakeToken = new object ();
  553. bool removed = app.RemoveTimeout (fakeToken);
  554. Assert.False (removed);
  555. }
  556. [Fact]
  557. public void TimedEvents_GetTimeout_Invalid_Token_Returns_Null ()
  558. {
  559. using IApplication app = Application.Create ();
  560. app.Init ("fake");
  561. var fakeToken = new object ();
  562. TimeSpan? actualTimeSpan = app.TimedEvents?.GetTimeout (fakeToken);
  563. Assert.Null (actualTimeSpan);
  564. }
  565. [Fact]
  566. public void TimedEvents_GetTimeout_Returns_Correct_TimeSpan ()
  567. {
  568. using IApplication app = Application.Create ();
  569. app.Init ("fake");
  570. TimeSpan expectedTimeSpan = TimeSpan.FromMilliseconds (500);
  571. object? token = app.AddTimeout (expectedTimeSpan, () => false);
  572. TimeSpan? actualTimeSpan = app.TimedEvents?.GetTimeout (token!);
  573. Assert.NotNull (actualTimeSpan);
  574. Assert.Equal (expectedTimeSpan, actualTimeSpan.Value);
  575. }
  576. [Fact]
  577. public void TimedEvents_StopAll_Clears_Timeouts ()
  578. {
  579. using IApplication app = Application.Create ();
  580. app.Init ("fake");
  581. var firedCount = 0;
  582. for (var i = 0; i < 10; i++)
  583. {
  584. app.AddTimeout (
  585. TimeSpan.FromMilliseconds (100),
  586. () =>
  587. {
  588. Interlocked.Increment (ref firedCount);
  589. return false;
  590. }
  591. );
  592. }
  593. Assert.NotEmpty (app.TimedEvents!.Timeouts);
  594. app.TimedEvents.StopAll ();
  595. Assert.Empty (app.TimedEvents.Timeouts);
  596. // Use iteration counter for safety
  597. var iterations = 0;
  598. app.Iteration += IterationHandler;
  599. try
  600. {
  601. app.Run<Runnable> ();
  602. Assert.Equal (0, firedCount);
  603. }
  604. finally
  605. {
  606. app.Iteration -= IterationHandler;
  607. }
  608. return;
  609. void IterationHandler (object? s, EventArgs<IApplication?> e)
  610. {
  611. iterations++;
  612. // Since all timeouts were cleared, just need enough iterations to prove they won't fire
  613. // With 100ms timeouts, give ~50 iterations which is more than enough
  614. if (iterations > 50)
  615. {
  616. app.RequestStop ();
  617. }
  618. }
  619. }
  620. [Fact]
  621. public void TimedEvents_Timeouts_Property_Is_Thread_Safe ()
  622. {
  623. using IApplication app = Application.Create ();
  624. app.Init ("fake");
  625. const int THREAD_COUNT = 10;
  626. var addedCount = 0;
  627. var tasksCompleted = new CountdownEvent (THREAD_COUNT);
  628. // Add timeouts from multiple threads using Invoke
  629. for (var i = 0; i < THREAD_COUNT; i++)
  630. {
  631. Task.Run (() =>
  632. {
  633. app.Invoke (() =>
  634. {
  635. // Add timeout with immediate execution
  636. app.AddTimeout (
  637. TimeSpan.Zero,
  638. () =>
  639. {
  640. Interlocked.Increment (ref addedCount);
  641. return false;
  642. }
  643. );
  644. tasksCompleted.Signal ();
  645. }
  646. );
  647. }
  648. );
  649. }
  650. // Use iteration counter to stop when all tasks complete
  651. var iterations = 0;
  652. app.Iteration += IterationHandler;
  653. try
  654. {
  655. app.Run<Runnable> ();
  656. // Verify we can safely access the Timeouts property from main thread
  657. int timeoutCount = app.TimedEvents?.Timeouts.Count ?? 0;
  658. // Verify no exceptions occurred
  659. Assert.True (timeoutCount >= 0, "Should be able to access Timeouts property without exception");
  660. // Verify all tasks completed and all timeouts fired
  661. Assert.True (tasksCompleted.IsSet, "All background tasks should have completed");
  662. Assert.Equal (THREAD_COUNT, addedCount);
  663. }
  664. finally
  665. {
  666. app.Iteration -= IterationHandler;
  667. tasksCompleted.Dispose ();
  668. }
  669. return;
  670. void IterationHandler (object? s, EventArgs<IApplication?> e)
  671. {
  672. iterations++;
  673. // Stop when all tasks completed and all timeouts fired, or safety limit
  674. if ((tasksCompleted.IsSet && addedCount >= THREAD_COUNT) || iterations > 200)
  675. {
  676. app.RequestStop ();
  677. }
  678. }
  679. }
  680. }