Skip to content

Commit 4424322

Browse files
authored
Merge pull request #1053 from bUnit-dev/release/v1.19
Release of new minor version v1.19
2 parents f9eb7a2 + c0f598f commit 4424322

33 files changed

+475
-407
lines changed

.config/dotnet-tools.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"isRoot": true,
44
"tools": {
55
"dotnet-serve": {
6-
"version": "1.7.125",
6+
"version": "1.10.155",
77
"commands": [
88
"dotnet-serve"
99
]

.editorconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,3 +505,4 @@ dotnet_diagnostic.S3459.severity = none # S3459: Unassigned members should be re
505505
dotnet_diagnostic.S3871.severity = none # S3871: Exception types should be "public"
506506
dotnet_diagnostic.S1186.severity = none # S1186: Methods should not be empty
507507
dotnet_diagnostic.S1133.severity = none # S1133: Deprecated code should be removed
508+
dotnet_diagnostic.S3963.severity = none # S3963: "static" fields should be initialized inline (covered by CA1810)

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ All notable changes to **bUnit** will be documented in this file. The project ad
66

77
## [Unreleased]
88

9+
### Fixed
10+
11+
- Custom elements with attributes throw `ArgumentException` with `MarkupMatches`. Reported by [@candritzky](https://github.com/candritzky). Fixed by [@linkdotnet](https://github.com/linkdotnet).
12+
13+
### Changed
14+
15+
- 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).
16+
917
## [1.18.4] - 2023-02-26
1018

1119
### Fixed

Directory.Build.props

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
<PropertyGroup Label="Compile settings">
2020
<Nullable>enable</Nullable>
2121
<LangVersion>11.0</LangVersion>
22-
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
22+
<AccelerateBuildsInVisualStudio>false</AccelerateBuildsInVisualStudio>
2323
<ImplicitUsings>enable</ImplicitUsings>
24-
<NoWarn>CA1014,NU5104</NoWarn>
24+
<NoWarn>CA1014,NU5104,NETSDK1138</NoWarn>
2525
<CheckEolTargetFramework>false</CheckEolTargetFramework>
2626

2727
<!-- Used by code coverage -->
@@ -50,7 +50,7 @@
5050
<!-- Shared code analyzers used for all projects in the solution -->
5151
<ItemGroup Label="Code Analyzers">
5252
<PackageReference Include="AsyncFixer" Version="1.6.0" PrivateAssets="All" />
53-
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.53.0.62665" PrivateAssets="All" />
53+
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.56.0.67649" PrivateAssets="All" />
5454
</ItemGroup>
5555

5656
<ItemGroup Label="Implicit usings"

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using System.ComponentModel;
2-
31
namespace Bunit.Extensions.WaitForHelpers;
42

53
/// <summary>

src/bunit.core/Rendering/RootComponent.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ internal sealed class RootComponent : IComponent
1212

1313
public void Attach(RenderHandle renderHandle) => this.renderHandle = renderHandle;
1414

15-
public Task SetParametersAsync(ParameterView parameters) => throw new InvalidOperationException($"{nameof(RootComponent)} shouldn't receive any parameters");
15+
public Task SetParametersAsync(ParameterView parameters)
16+
=> throw new InvalidOperationException($"{nameof(RootComponent)} shouldn't receive any parameters");
1617

1718
public void Render() => renderHandle.Render(renderFragment);
1819

1920
public void Detach() => renderHandle.Render(_ => { });
20-
}
21+
}

src/bunit.core/Rendering/RootRenderTree.cs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,9 @@ public RenderFragment Wrap(RenderFragment target)
9191
public int GetCountOf<TComponent>()
9292
where TComponent : IComponent
9393
{
94-
var result = 0;
9594
var countType = typeof(TComponent);
9695

97-
for (int i = 0; i < registrations.Count; i++)
98-
{
99-
if (countType == registrations[i].ComponentType)
100-
result++;
101-
}
102-
103-
return result;
96+
return registrations.Count(t => countType == t.ComponentType);
10497
}
10598

10699
private static RenderFragment<RenderFragment> CreateRenderFragmentBuilder<TComponent>(Action<ComponentParameterCollectionBuilder<TComponent>>? parameterBuilder)

src/bunit.core/Rendering/TestRenderer.cs

Lines changed: 100 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ namespace Bunit.Rendering;
88
/// </summary>
99
public class TestRenderer : Renderer, ITestRenderer
1010
{
11+
private readonly object renderTreeUpdateLock = new();
12+
private readonly SynchronizationContext? usersSyncContext = SynchronizationContext.Current;
1113
private readonly Dictionary<int, IRenderedFragmentBase> renderedComponents = new();
1214
private readonly List<RootComponent> rootComponents = new();
1315
private readonly ILogger<TestRenderer> logger;
@@ -79,34 +81,39 @@ public Task DispatchEventAsync(
7981
if (fieldInfo is null)
8082
throw new ArgumentNullException(nameof(fieldInfo));
8183

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

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

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

109-
return result;
115+
return result;
116+
}
110117
}
111118

112119
/// <inheritdoc/>
@@ -124,7 +131,6 @@ public IReadOnlyList<IRenderedComponentBase<TComponent>> FindComponents<TCompone
124131
where TComponent : IComponent
125132
=> FindComponents<TComponent>(parentComponent, int.MaxValue);
126133

127-
128134
/// <inheritdoc />
129135
public void DisposeComponents()
130136
{
@@ -151,13 +157,54 @@ public void DisposeComponents()
151157
AssertNoUnhandledExceptions();
152158
}
153159

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+
154173
/// <inheritdoc/>
155174
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
156175
{
157-
logger.LogNewRenderBatchReceived();
176+
if (usersSyncContext is not null && usersSyncContext != SynchronizationContext.Current)
177+
{
178+
// The users' sync context, typically one established by
179+
// xUnit or another testing framework is used to update any
180+
// rendered fragments/dom trees and trigger WaitForX handlers.
181+
// This ensures that changes to DOM observed inside a WaitForX handler
182+
// will also be visible outside a WaitForX handler, since
183+
// they will be running in the same sync context. The theory is that
184+
// this should mitigate the issues where Blazor's dispatcher/thread is used
185+
// to verify an assertion inside a WaitForX handler, and another thread is
186+
// used again to access the DOM/repeat the assertion, where the change
187+
// may not be visible yet (another theory about why that may happen is different
188+
// CPU cache updates not happening immediately).
189+
//
190+
// There is no guarantee a caller/test framework has set a sync context.
191+
usersSyncContext.Send(static (state) =>
192+
{
193+
var (renderBatch, renderer) = ((RenderBatch, TestRenderer))state!;
194+
renderer.UpdateDisplay(renderBatch);
195+
}, (renderBatch, this));
196+
}
197+
else
198+
{
199+
UpdateDisplay(renderBatch);
200+
}
201+
202+
return Task.CompletedTask;
203+
}
158204

205+
private void UpdateDisplay(in RenderBatch renderBatch)
206+
{
159207
RenderCount++;
160-
161208
var renderEvent = new RenderEvent(renderBatch, new RenderTreeFrameDictionary());
162209

163210
// removes disposed components
@@ -177,12 +224,12 @@ protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
177224
// notify each rendered component about the render
178225
foreach (var (key, rc) in renderedComponents.ToArray())
179226
{
180-
logger.LogComponentRendered(rc.ComponentId);
181-
182227
LoadRenderTreeFrames(rc.ComponentId, renderEvent.Frames);
183228

184229
rc.OnRender(renderEvent);
185230

231+
logger.LogComponentRendered(rc.ComponentId);
232+
186233
// RC can replace the instance of the component it is bound
187234
// to while processing the update event.
188235
if (key != rc.ComponentId)
@@ -191,10 +238,6 @@ protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
191238
renderedComponents.Add(rc.ComponentId, rc);
192239
}
193240
}
194-
195-
logger.LogChangedComponentsMarkupUpdated();
196-
197-
return Task.CompletedTask;
198241
}
199242

200243
/// <inheritdoc/>
@@ -255,63 +298,56 @@ private IReadOnlyList<IRenderedComponentBase<TComponent>> FindComponents<TCompon
255298
if (parentComponent is null)
256299
throw new ArgumentNullException(nameof(parentComponent));
257300

258-
// Ensure FindComponents runs on the same thread as the renderer,
259-
// and that the renderer does not perform any renders while
260-
// FindComponents is traversing the current render tree.
261-
// Without this, the render tree could change while FindComponentsInternal
262-
// is traversing down the render tree, with indeterministic as a results.
263-
return Dispatcher.InvokeAsync(() =>
264-
{
265-
var result = new List<IRenderedComponentBase<TComponent>>();
266-
var framesCollection = new RenderTreeFrameDictionary();
301+
var result = new List<IRenderedComponentBase<TComponent>>();
302+
var framesCollection = new RenderTreeFrameDictionary();
267303

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

270-
return result;
311+
return result;
271312

272-
void FindComponentsInRenderTree(int componentId)
273-
{
274-
var frames = GetOrLoadRenderTreeFrame(framesCollection, componentId);
313+
void FindComponentsInRenderTree(int componentId)
314+
{
315+
var frames = GetOrLoadRenderTreeFrame(framesCollection, componentId);
275316

276-
for (var i = 0; i < frames.Count; i++)
317+
for (var i = 0; i < frames.Count; i++)
318+
{
319+
ref var frame = ref frames.Array[i];
320+
if (frame.FrameType == RenderTreeFrameType.Component)
277321
{
278-
ref var frame = ref frames.Array[i];
279-
if (frame.FrameType == RenderTreeFrameType.Component)
322+
if (frame.Component is TComponent component)
280323
{
281-
if (frame.Component is TComponent component)
282-
{
283-
result.Add(GetOrCreateRenderedComponent(framesCollection, frame.ComponentId, component));
284-
285-
if (result.Count == resultLimit)
286-
return;
287-
}
288-
289-
FindComponentsInRenderTree(frame.ComponentId);
324+
result.Add(GetOrCreateRenderedComponent(framesCollection, frame.ComponentId, component));
290325

291326
if (result.Count == resultLimit)
292327
return;
293328
}
329+
330+
FindComponentsInRenderTree(frame.ComponentId);
331+
332+
if (result.Count == resultLimit)
333+
return;
294334
}
295335
}
296-
}).GetAwaiter().GetResult();
336+
}
297337
}
298338

299339
IRenderedComponentBase<TComponent> GetOrCreateRenderedComponent<TComponent>(RenderTreeFrameDictionary framesCollection, int componentId, TComponent component)
300340
where TComponent : IComponent
301341
{
302-
IRenderedComponentBase<TComponent> result;
303-
304342
if (renderedComponents.TryGetValue(componentId, out var renderedComponent))
305343
{
306-
result = (IRenderedComponentBase<TComponent>)renderedComponent;
307-
}
308-
else
309-
{
310-
LoadRenderTreeFrames(componentId, framesCollection);
311-
result = activator.CreateRenderedComponent(componentId, component, framesCollection);
312-
renderedComponents.Add(result.ComponentId, result);
344+
return (IRenderedComponentBase<TComponent>)renderedComponent;
313345
}
314346

347+
LoadRenderTreeFrames(componentId, framesCollection);
348+
var result = activator.CreateRenderedComponent(componentId, component, framesCollection);
349+
renderedComponents.Add(result.ComponentId, result);
350+
315351
return result;
316352
}
317353

src/bunit.core/TestContextBase.cs

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,7 @@ public abstract class TestContextBase : IDisposable
1313
/// <summary>
1414
/// Gets the renderer used by the test context.
1515
/// </summary>
16-
public ITestRenderer Renderer
17-
{
18-
get
19-
{
20-
if (testRenderer is null)
21-
{
22-
testRenderer = Services.GetRequiredService<ITestRenderer>();
23-
}
24-
25-
return testRenderer;
26-
}
27-
}
16+
public ITestRenderer Renderer => testRenderer ??= Services.GetRequiredService<ITestRenderer>();
2817

2918
/// <summary>
3019
/// Gets the service collection and service provider that is used when a
@@ -90,7 +79,7 @@ protected virtual void Dispose(bool disposing)
9079

9180
// Ignore the async task as GetAwaiter().GetResult() can cause deadlock
9281
// and implementing IAsyncDisposable in TestContext will be a breaking change.
93-
//
82+
//
9483
// NOTE: This has to be called before Services.Dispose().
9584
// If there are IAsyncDisposable services registered, calling Dispose first
9685
// causes the service provider to throw an exception.

src/bunit.core/TestServiceProvider.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.Collections;
2-
using Microsoft.Extensions.DependencyInjection;
32

43
namespace Bunit;
54

0 commit comments

Comments
 (0)