Skip to content

Commit 85cb899

Browse files
committed
use task.delay instead of timer
1 parent 8a1ff5c commit 85cb899

File tree

1 file changed

+78
-81
lines changed

1 file changed

+78
-81
lines changed

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

Lines changed: 78 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ namespace Bunit.Extensions.WaitForHelpers;
99
/// </summary>
1010
public abstract class WaitForHelper<T> : IDisposable
1111
{
12-
//// private readonly object lockObject = new();
13-
private readonly Timer timer;
1412
private readonly TaskCompletionSource<T> checkPassedCompletionSource;
1513
private readonly Func<(bool CheckPassed, T Content)> completeChecker;
1614
private readonly IRenderedFragmentBase renderedFragment;
@@ -40,60 +38,108 @@ public abstract class WaitForHelper<T> : IDisposable
4038
/// </summary>
4139
public Task<T> WaitTask { get; }
4240

43-
4441
/// <summary>
4542
/// Initializes a new instance of the <see cref="WaitForHelper{T}"/> class.
4643
/// </summary>
47-
protected WaitForHelper(IRenderedFragmentBase renderedFragment, Func<(bool CheckPassed, T Content)> completeChecker, TimeSpan? timeout = null)
44+
protected WaitForHelper(
45+
IRenderedFragmentBase renderedFragment,
46+
Func<(bool CheckPassed, T Content)> completeChecker,
47+
TimeSpan? timeout = null)
4848
{
4949
this.renderedFragment = renderedFragment ?? throw new ArgumentNullException(nameof(renderedFragment));
5050
this.completeChecker = completeChecker ?? throw new ArgumentNullException(nameof(completeChecker));
5151
logger = renderedFragment.Services.CreateLogger<WaitForHelper<T>>();
52+
checkPassedCompletionSource = new TaskCompletionSource<T>();
5253

53-
var renderer = renderedFragment.Services.GetRequiredService<ITestRenderer>();
54-
var renderException = renderer
55-
.UnhandledException
56-
.ContinueWith(x => Task.FromException<T>(x.Result), CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current)
57-
.Unwrap();
54+
WaitTask = CreateWaitTask(renderedFragment, timeout);
5855

59-
checkPassedCompletionSource = new TaskCompletionSource<T>();
60-
WaitTask = Task.WhenAny(checkPassedCompletionSource.Task, renderException).Unwrap();
56+
InitializeWaiting();
57+
}
58+
59+
/// <summary>
60+
/// Disposes the wait helper and cancels the any ongoing waiting, if it is not
61+
/// already in one of the other completed states.
62+
/// </summary>
63+
public void Dispose()
64+
{
65+
Dispose(disposing: true);
66+
GC.SuppressFinalize(this);
67+
}
68+
69+
/// <summary>
70+
/// Disposes of the wait task and related logic.
71+
/// </summary>
72+
/// <remarks>
73+
/// The disposing parameter should be false when called from a finalizer, and true when called from the
74+
/// <see cref="Dispose()"/> method. In other words, it is true when deterministically called and false when non-deterministically called.
75+
/// </remarks>
76+
/// <param name="disposing">Set to true if called from <see cref="Dispose()"/>, false if called from a finalizer.f.</param>
77+
protected virtual void Dispose(bool disposing)
78+
{
79+
if (isDisposed || !disposing)
80+
return;
6181

62-
timer = new Timer(OnTimeout, this, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
82+
isDisposed = true;
83+
checkPassedCompletionSource.TrySetCanceled();
84+
renderedFragment.OnAfterRender -= OnAfterRender;
85+
logger.LogWaiterDisposed(renderedFragment.ComponentId);
86+
}
6387

88+
private void InitializeWaiting()
89+
{
6490
if (!WaitTask.IsCompleted)
6591
{
92+
var renderCountAtSubscribeTime = renderedFragment.RenderCount;
93+
94+
// Before subscribing to renderedFragment.OnAfterRender,
95+
// we need to make sure that the desired state has not already been reached.
6696
OnAfterRender(this, EventArgs.Empty);
67-
this.renderedFragment.OnAfterRender += OnAfterRender;
68-
OnAfterRender(this, EventArgs.Empty);
69-
StartTimer(timeout);
97+
98+
SubscribeToOnAfterRender();
99+
100+
// If the render count from before subscribing has changes
101+
// till now, we need to do trigger another check, since
102+
// the render may have happened asynchronously and before
103+
// the subscription was set up.
104+
if (renderCountAtSubscribeTime < renderedFragment.RenderCount)
105+
{
106+
OnAfterRender(this, EventArgs.Empty);
107+
}
70108
}
71109
}
72110

73-
private void StartTimer(TimeSpan? timeout)
111+
private Task<T> CreateWaitTask(IRenderedFragmentBase renderedFragment, TimeSpan? timeout)
74112
{
75-
if (isDisposed)
76-
return;
113+
var renderer = renderedFragment.Services.GetRequiredService<ITestRenderer>();
77114

78-
////lock (lockObject)
79-
////{
80-
//// if (isDisposed)
81-
//// return;
115+
var renderException = renderer
116+
.UnhandledException
117+
.ContinueWith(
118+
x => Task.FromException<T>(x.Result),
119+
CancellationToken.None,
120+
TaskContinuationOptions.OnlyOnRanToCompletion,
121+
TaskScheduler.Current)
122+
.Unwrap();
82123

83-
timer.Change(GetRuntimeTimeout(timeout), Timeout.InfiniteTimeSpan);
84-
////}
124+
var timeoutTask = Task.Delay(GetRuntimeTimeout(timeout))
125+
.ContinueWith(
126+
x => Task.FromException<T>(new WaitForFailedException(TimeoutErrorMessage, capturedException)),
127+
CancellationToken.None,
128+
TaskContinuationOptions.OnlyOnRanToCompletion,
129+
TaskScheduler.Current)
130+
.Unwrap();
131+
132+
return Task.WhenAny(
133+
checkPassedCompletionSource.Task,
134+
renderException,
135+
timeoutTask).Unwrap();
85136
}
86137

87138
private void OnAfterRender(object? sender, EventArgs args)
88139
{
89140
if (isDisposed)
90141
return;
91142

92-
////lock (lockObject)
93-
////{
94-
//// if (isDisposed)
95-
//// return;
96-
97143
try
98144
{
99145
logger.LogCheckingWaitCondition(renderedFragment.ComponentId);
@@ -121,61 +167,12 @@ private void OnAfterRender(object? sender, EventArgs args)
121167
Dispose();
122168
}
123169
}
124-
////}
125-
}
126-
127-
private void OnTimeout(object? state)
128-
{
129-
if (isDisposed)
130-
return;
131-
132-
////lock (lockObject)
133-
////{
134-
//// if (isDisposed)
135-
//// return;
136-
137-
logger.LogWaiterTimedOut(renderedFragment.ComponentId);
138-
139-
checkPassedCompletionSource.TrySetException(new WaitForFailedException(TimeoutErrorMessage, capturedException));
140-
141-
Dispose();
142-
////}
143-
}
144-
145-
/// <summary>
146-
/// Disposes the wait helper and cancels the any ongoing waiting, if it is not
147-
/// already in one of the other completed states.
148-
/// </summary>
149-
public void Dispose()
150-
{
151-
Dispose(disposing: true);
152-
GC.SuppressFinalize(this);
153170
}
154171

155-
/// <summary>
156-
/// Disposes of the wait task and related logic.
157-
/// </summary>
158-
/// <remarks>
159-
/// The disposing parameter should be false when called from a finalizer, and true when called from the
160-
/// <see cref="Dispose()"/> method. In other words, it is true when deterministically called and false when non-deterministically called.
161-
/// </remarks>
162-
/// <param name="disposing">Set to true if called from <see cref="Dispose()"/>, false if called from a finalizer.f.</param>
163-
protected virtual void Dispose(bool disposing)
172+
private void SubscribeToOnAfterRender()
164173
{
165-
if (isDisposed || !disposing)
166-
return;
167-
168-
////lock (lockObject)
169-
////{
170-
//// if (isDisposed)
171-
//// return;
172-
173-
isDisposed = true;
174-
renderedFragment.OnAfterRender -= OnAfterRender;
175-
timer.Dispose();
176-
checkPassedCompletionSource.TrySetCanceled();
177-
logger.LogWaiterDisposed(renderedFragment.ComponentId);
178-
////}
174+
if (!isDisposed && !WaitTask.IsCompleted)
175+
renderedFragment.OnAfterRender += OnAfterRender;
179176
}
180177

181178
private static TimeSpan GetRuntimeTimeout(TimeSpan? timeout)

0 commit comments

Comments
 (0)