Skip to content

Commit 71c5960

Browse files
committed
fix: always perform at least one check in WaitFor to avoid timer not getting triggered before check gets a chance to complete
1 parent 96dba79 commit 71c5960

File tree

1 file changed

+33
-21
lines changed

1 file changed

+33
-21
lines changed

src/bunit.core/Extensions/WaitForHelpers/WaitForHelper.cs

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ namespace Bunit.Extensions.WaitForHelpers;
1010
/// </summary>
1111
public abstract class WaitForHelper<T> : IDisposable
1212
{
13-
private readonly Timer timer;
1413
private readonly TaskCompletionSource<T> checkPassedCompletionSource;
1514
private readonly Func<(bool CheckPassed, T Content)> completeChecker;
1615
private readonly IRenderedFragmentBase renderedFragment;
1716
private readonly ILogger<WaitForHelper<T>> logger;
1817
private readonly TestRenderer renderer;
18+
private readonly Timer? timer;
1919
private bool isDisposed;
2020
private int checkCount;
2121
private Exception? capturedException;
@@ -60,25 +60,37 @@ protected WaitForHelper(
6060
.Renderer;
6161
checkPassedCompletionSource = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
6262

63+
// Create the wait task and run the initial check
64+
// and subscribe to the OnAfterRender event.
65+
// This must happen before the timer is started,
66+
// as the check happens inside the renderers synchronization context,
67+
// and that may be blocked longer than the timeout on overloaded systems,
68+
// resulting in the timer completing before a single check has
69+
// has a chance to complete.
6370
WaitTask = CreateWaitTask();
64-
timer = new Timer(
65-
static (state) =>
66-
{
67-
var @this = (WaitForHelper<T>)state!;
68-
@this.logger.LogWaiterTimedOut(@this.renderedFragment.ComponentId);
69-
@this.checkPassedCompletionSource.TrySetException(
70-
new WaitForFailedException(
71-
@this.TimeoutErrorMessage ?? string.Empty,
72-
@this.checkCount,
73-
@this.renderedFragment.RenderCount,
74-
@this.renderer.RenderCount,
75-
@this.capturedException));
76-
},
77-
this,
78-
GetRuntimeTimeout(timeout),
79-
Timeout.InfiniteTimeSpan);
80-
81-
InitializeWaiting();
71+
CheckAndInitializeWaiting();
72+
73+
// If the initial check did not complete successfully,
74+
// start the timer and recheck after every render until the timer expires.
75+
if (!WaitTask.IsCompleted)
76+
{
77+
timer = new Timer(
78+
static (state) =>
79+
{
80+
var @this = (WaitForHelper<T>)state!;
81+
@this.logger.LogWaiterTimedOut(@this.renderedFragment.ComponentId);
82+
@this.checkPassedCompletionSource.TrySetException(
83+
new WaitForFailedException(
84+
@this.TimeoutErrorMessage ?? string.Empty,
85+
@this.checkCount,
86+
@this.renderedFragment.RenderCount,
87+
@this.renderer.RenderCount,
88+
@this.capturedException));
89+
},
90+
this,
91+
GetRuntimeTimeout(timeout),
92+
Timeout.InfiniteTimeSpan);
93+
}
8294
}
8395

8496
/// <summary>
@@ -105,13 +117,13 @@ protected virtual void Dispose(bool disposing)
105117
return;
106118

107119
isDisposed = true;
108-
timer.Dispose();
120+
timer?.Dispose();
109121
checkPassedCompletionSource.TrySetCanceled();
110122
renderedFragment.OnAfterRender -= OnAfterRender;
111123
logger.LogWaiterDisposed(renderedFragment.ComponentId);
112124
}
113125

114-
private void InitializeWaiting()
126+
private void CheckAndInitializeWaiting()
115127
{
116128
if (!WaitTask.IsCompleted)
117129
{

0 commit comments

Comments
 (0)