Skip to content

Commit

Permalink
fix: synchronize code that reads/writes to the internal render tree i…
Browse files Browse the repository at this point in the history
…n TestRenderer/Renderer
  • Loading branch information
egil committed Apr 11, 2023
1 parent c90c0a0 commit 7c15692
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 22 deletions.
1 change: 0 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ All notable changes to **bUnit** will be documented in this file. The project ad
### Changed

- 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).
- Changed default "WaitFor" timeout to 10 seconds. By [@egil](https://github.com/egil).

## [1.18.4] - 2023-02-26

Expand Down
65 changes: 45 additions & 20 deletions src/bunit.core/Rendering/TestRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Bunit.Rendering;
/// </summary>
public class TestRenderer : Renderer, ITestRenderer
{
private readonly object renderTreeUpdateLock = new();
private readonly SynchronizationContext? usersSyncContext = SynchronizationContext.Current;
private readonly Dictionary<int, IRenderedFragmentBase> renderedComponents = new();
private readonly List<RootComponent> rootComponents = new();
Expand Down Expand Up @@ -80,33 +81,39 @@ public Task DispatchEventAsync(
if (fieldInfo is null)
throw new ArgumentNullException(nameof(fieldInfo));

var result = Dispatcher.InvokeAsync(() =>
// Calling base.DispatchEventAsync updates the render tree
// if the event contains associated data.
lock (renderTreeUpdateLock)
{
ResetUnhandledException();

try
{
return base.DispatchEventAsync(eventHandlerId, fieldInfo, eventArgs);
}
catch (ArgumentException ex) when (string.Equals(ex.Message, $"There is no event handler associated with this event. EventId: '{eventHandlerId}'. (Parameter 'eventHandlerId')", StringComparison.Ordinal))
var result = Dispatcher.InvokeAsync(() =>
{
if (ignoreUnknownEventHandlers)
ResetUnhandledException();

try
{
return Task.CompletedTask;
return base.DispatchEventAsync(eventHandlerId, fieldInfo, eventArgs);
}
catch (ArgumentException ex) when (string.Equals(ex.Message, $"There is no event handler associated with this event. EventId: '{eventHandlerId}'. (Parameter 'eventHandlerId')", StringComparison.Ordinal))
{
if (ignoreUnknownEventHandlers)
{
return Task.CompletedTask;
}

var betterExceptionMsg = new UnknownEventHandlerIdException(eventHandlerId, fieldInfo, ex);
return Task.FromException(betterExceptionMsg);
}
});

var betterExceptionMsg = new UnknownEventHandlerIdException(eventHandlerId, fieldInfo, ex);
return Task.FromException(betterExceptionMsg);
if (result.IsFaulted && result.Exception is not null)
{
HandleException(result.Exception);
}
});

if (result.IsFaulted && result.Exception is not null)
{
HandleException(result.Exception);
}
AssertNoUnhandledExceptions();

AssertNoUnhandledExceptions();
return result;
return result;
}
}

/// <inheritdoc/>
Expand Down Expand Up @@ -150,6 +157,19 @@ public void DisposeComponents()
AssertNoUnhandledExceptions();
}

/// <inheritdoc/>
protected override void ProcessPendingRender()
{
// Blocks updates to the renderers internal render tree
// while the render tree is being read elsewhere.
// base.ProcessPendingRender calls UpdateDisplayAsync,
// so there is no need to lock in that method.
lock (renderTreeUpdateLock)
{
base.ProcessPendingRender();
}
}

/// <inheritdoc/>
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
{
Expand Down Expand Up @@ -281,7 +301,12 @@ private IReadOnlyList<IRenderedComponentBase<TComponent>> FindComponents<TCompon
var result = new List<IRenderedComponentBase<TComponent>>();
var framesCollection = new RenderTreeFrameDictionary();

FindComponentsInRenderTree(parentComponent.ComponentId);
// Blocks the renderer from changing the render tree
// while this method searches through it.
lock (renderTreeUpdateLock)
{
FindComponentsInRenderTree(parentComponent.ComponentId);
}

return result;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Bunit.Extensions.WaitForHelpers;

public class RenderedFragmentWaitForElementsHelperExtensionsAsyncTest : TestContext
{
private readonly static TimeSpan WaitForTestTimeout = TimeSpan.FromMilliseconds(5);
private readonly static TimeSpan WaitForTestTimeout = TimeSpan.FromMilliseconds(100);

public RenderedFragmentWaitForElementsHelperExtensionsAsyncTest(ITestOutputHelper testOutput)
{
Expand Down

0 comments on commit 7c15692

Please sign in to comment.