1- using System . Diagnostics ;
1+ using System . Diagnostics ;
22using System . Drawing ;
3+ using System . Runtime . ExceptionServices ;
34using System . Text ;
45using Microsoft . Extensions . Logging ;
56using Terminal . Gui . Time ;
@@ -108,7 +109,8 @@ public AppTestHelper (string driverName, TextWriter? logWriter = null, TimeSpan?
108109 }
109110
110111 /// <summary>
111- /// Constructor for tests that need to run the application with Application.Run.
112+ /// Constructor for tests that need to run the application with IApplication.RunAsync.
113+ /// The runnable observes the helper's linked cancellation token, including timeout cancellation.
112114 /// </summary>
113115 internal AppTestHelper ( Func < IRunnable > runnableBuilder , int width , int height , string driverName , TextWriter ? logWriter = null , TimeSpan ? timeout = null )
114116 {
@@ -120,7 +122,7 @@ internal AppTestHelper (Func<IRunnable> runnableBuilder, int width, int height,
120122 CommonInit ( width , height , timeout ) ;
121123
122124 // Start the application in a background thread
123- _runTask = Task . Run ( ( ) =>
125+ _runTask = Task . Run ( async ( ) =>
124126 {
125127 _loggerScope = Logging . PushLogger ( _testLogger ! ) ;
126128
@@ -140,27 +142,36 @@ internal AppTestHelper (Func<IRunnable> runnableBuilder, int width, int height,
140142 _booting . Release ( ) ;
141143 }
142144
143- if ( App is { Initialized : true } )
145+ if ( App is { Initialized : true } app )
144146 {
145- IRunnable runnable = runnableBuilder ( ) ;
147+ IRunnable ? runnable = null ;
146148
147- runnable . IsRunningChanged += ( s , e ) =>
148- {
149- if ( ! e . Value )
150- {
151- Finished = true ;
152- }
153- } ;
154- App ? . Run ( runnable ) ; // This will block, but it's on a background thread now
149+ try
150+ {
151+ runnable = runnableBuilder ( ) ;
155152
156- if ( runnable is View runnableView )
153+ runnable . IsRunningChanged += ( _ , e ) =>
154+ {
155+ if ( ! e . Value )
156+ {
157+ Finished = true ;
158+ }
159+ } ;
160+
161+ CancellationToken helperCancellationToken = _ansiInput . ExternalCancellationTokenSource ! . Token ;
162+ await app . RunAsync ( runnable , helperCancellationToken ) ;
163+ }
164+ finally
157165 {
158- runnableView . Dispose ( ) ;
166+ if ( runnable is View runnableView )
167+ {
168+ runnableView . Dispose ( ) ;
169+ }
170+
171+ //Logging.Trace ("Application.Run completed");
172+ app . Dispose ( ) ;
173+ _runCancellationTokenSource . Cancel ( ) ;
159174 }
160-
161- //Logging.Trace ("Application.Run completed");
162- App ? . Dispose ( ) ;
163- _runCancellationTokenSource . Cancel ( ) ;
164175 }
165176 }
166177 catch ( OperationCanceledException )
@@ -321,29 +332,42 @@ public AppTestHelper WaitIteration (Action<IApplication>? action = null)
321332 {
322333 action = app => { } ;
323334 }
335+
324336 CancellationTokenSource ctsActionCompleted = new ( ) ;
337+ Exception ? actionException = null ;
325338
326339 App ? . Invoke ( app =>
327- {
328- try
329- {
330- action ( app ) ;
331-
332- //Logging.Trace ("Action completed");
333- ctsActionCompleted . Cancel ( ) ;
334- }
335- catch ( Exception e )
336- {
337- Logging . Warning ( $ "Action failed with exception: { e } ") ;
338- _backgroundException = e ;
339- _ansiInput . ExternalCancellationTokenSource ? . Cancel ( ) ;
340- }
341- } ) ;
342-
343- // Blocks until either the token or the hardStopToken is cancelled.
344- // With linked tokens, we only need to wait on _runCancellationTokenSource and ctsLocal
345- // ExternalCancellationTokenSource is redundant because it's linked to _runCancellationTokenSource
346- WaitHandle . WaitAny ( [ _runCancellationTokenSource . Token . WaitHandle , ctsActionCompleted . Token . WaitHandle ] ) ;
340+ {
341+ try
342+ {
343+ action ( app ) ;
344+
345+ //Logging.Trace ("Action completed");
346+ ctsActionCompleted . Cancel ( ) ;
347+ }
348+ catch ( Exception e )
349+ {
350+ Logging . Warning ( $ "Action failed with exception: { e } ") ;
351+ _backgroundException = e ;
352+ actionException = e ;
353+ _ansiInput . ExternalCancellationTokenSource ? . Cancel ( ) ;
354+ }
355+ } ) ;
356+
357+ // Blocks until either the action completes, the run stops, or the timeout/hard-stop token is cancelled.
358+ WaitHandle [ ] waitHandles =
359+ [
360+ _runCancellationTokenSource . Token . WaitHandle ,
361+ ctsActionCompleted . Token . WaitHandle ,
362+ _ansiInput . ExternalCancellationTokenSource ! . Token . WaitHandle
363+ ] ;
364+
365+ WaitHandle . WaitAny ( waitHandles ) ;
366+
367+ if ( actionException is { } )
368+ {
369+ ExceptionDispatchInfo . Capture ( actionException ) . Throw ( ) ;
370+ }
347371
348372 // Logging.Trace ($"Return from WaitIteration");
349373 return this ;
@@ -415,6 +439,17 @@ public AppTestHelper AnsiScreenShot (string title, TextWriter? writer) =>
415439 writer ? . WriteLine ( text ) ;
416440 } ) ;
417441
442+ /// <summary>
443+ /// Cancels the linked token observed by the running application.
444+ /// </summary>
445+ /// <returns></returns>
446+ public AppTestHelper CancelRun ( )
447+ {
448+ _ansiInput . ExternalCancellationTokenSource ? . Cancel ( ) ;
449+
450+ return this ;
451+ }
452+
418453 /// <summary>
419454 /// Stops the application and waits for the background thread to exit.
420455 /// </summary>
0 commit comments