Skip to content

Commit 7c15692

Browse files
committed
fix: synchronize code that reads/writes to the internal render tree in TestRenderer/Renderer
1 parent c90c0a0 commit 7c15692

3 files changed

Lines changed: 46 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ All notable changes to **bUnit** will be documented in this file. The project ad
1010
### Changed
1111

1212
- Changed test renderer such that updates to rendered components markup happen in the same synchronization context as the test framework is using (if any), if any, to avoid memory race conditions. By [@egil](https://github.com/egil).
13-
- Changed default "WaitFor" timeout to 10 seconds. By [@egil](https://github.com/egil).
1413

1514
## [1.18.4] - 2023-02-26
1615

src/bunit.core/Rendering/TestRenderer.cs

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace Bunit.Rendering;
88
/// </summary>
99
public class TestRenderer : Renderer, ITestRenderer
1010
{
11+
private readonly object renderTreeUpdateLock = new();
1112
private readonly SynchronizationContext? usersSyncContext = SynchronizationContext.Current;
1213
private readonly Dictionary<int, IRenderedFragmentBase> renderedComponents = new();
1314
private readonly List<RootComponent> rootComponents = new();
@@ -80,33 +81,39 @@ public Task DispatchEventAsync(
8081
if (fieldInfo is null)
8182
throw new ArgumentNullException(nameof(fieldInfo));
8283

83-
var result = Dispatcher.InvokeAsync(() =>
84+
// Calling base.DispatchEventAsync updates the render tree
85+
// if the event contains associated data.
86+
lock (renderTreeUpdateLock)
8487
{
85-
ResetUnhandledException();
86-
87-
try
88-
{
89-
return base.DispatchEventAsync(eventHandlerId, fieldInfo, eventArgs);
90-
}
91-
catch (ArgumentException ex) when (string.Equals(ex.Message, $"There is no event handler associated with this event. EventId: '{eventHandlerId}'. (Parameter 'eventHandlerId')", StringComparison.Ordinal))
88+
var result = Dispatcher.InvokeAsync(() =>
9289
{
93-
if (ignoreUnknownEventHandlers)
90+
ResetUnhandledException();
91+
92+
try
9493
{
95-
return Task.CompletedTask;
94+
return base.DispatchEventAsync(eventHandlerId, fieldInfo, eventArgs);
9695
}
96+
catch (ArgumentException ex) when (string.Equals(ex.Message, $"There is no event handler associated with this event. EventId: '{eventHandlerId}'. (Parameter 'eventHandlerId')", StringComparison.Ordinal))
97+
{
98+
if (ignoreUnknownEventHandlers)
99+
{
100+
return Task.CompletedTask;
101+
}
102+
103+
var betterExceptionMsg = new UnknownEventHandlerIdException(eventHandlerId, fieldInfo, ex);
104+
return Task.FromException(betterExceptionMsg);
105+
}
106+
});
97107

98-
var betterExceptionMsg = new UnknownEventHandlerIdException(eventHandlerId, fieldInfo, ex);
99-
return Task.FromException(betterExceptionMsg);
108+
if (result.IsFaulted && result.Exception is not null)
109+
{
110+
HandleException(result.Exception);
100111
}
101-
});
102112

103-
if (result.IsFaulted && result.Exception is not null)
104-
{
105-
HandleException(result.Exception);
106-
}
113+
AssertNoUnhandledExceptions();
107114

108-
AssertNoUnhandledExceptions();
109-
return result;
115+
return result;
116+
}
110117
}
111118

112119
/// <inheritdoc/>
@@ -150,6 +157,19 @@ public void DisposeComponents()
150157
AssertNoUnhandledExceptions();
151158
}
152159

160+
/// <inheritdoc/>
161+
protected override void ProcessPendingRender()
162+
{
163+
// Blocks updates to the renderers internal render tree
164+
// while the render tree is being read elsewhere.
165+
// base.ProcessPendingRender calls UpdateDisplayAsync,
166+
// so there is no need to lock in that method.
167+
lock (renderTreeUpdateLock)
168+
{
169+
base.ProcessPendingRender();
170+
}
171+
}
172+
153173
/// <inheritdoc/>
154174
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
155175
{
@@ -281,7 +301,12 @@ private IReadOnlyList<IRenderedComponentBase<TComponent>> FindComponents<TCompon
281301
var result = new List<IRenderedComponentBase<TComponent>>();
282302
var framesCollection = new RenderTreeFrameDictionary();
283303

284-
FindComponentsInRenderTree(parentComponent.ComponentId);
304+
// Blocks the renderer from changing the render tree
305+
// while this method searches through it.
306+
lock (renderTreeUpdateLock)
307+
{
308+
FindComponentsInRenderTree(parentComponent.ComponentId);
309+
}
285310

286311
return result;
287312

tests/bunit.web.tests/Extensions/WaitForHelpers/RenderedFragmentWaitForElementsHelperExtensions.Async.Test.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Bunit.Extensions.WaitForHelpers;
55

66
public class RenderedFragmentWaitForElementsHelperExtensionsAsyncTest : TestContext
77
{
8-
private readonly static TimeSpan WaitForTestTimeout = TimeSpan.FromMilliseconds(5);
8+
private readonly static TimeSpan WaitForTestTimeout = TimeSpan.FromMilliseconds(100);
99

1010
public RenderedFragmentWaitForElementsHelperExtensionsAsyncTest(ITestOutputHelper testOutput)
1111
{

0 commit comments

Comments
 (0)