Skip to content

Commit 6067580

Browse files
authored
fix: prevent null WrappedElement during concurrent component re-renders (#1946)
1 parent 69ad211 commit 6067580

4 files changed

Lines changed: 51 additions & 15 deletions

File tree

‎CHANGELOG.md‎

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

99
### Fixed
1010

11+
- `WrapperBase.WrappedElement` could return `null` and throw `NullReferenceException` when an element was accessed concurrently with component re-renders. Reported by [@psu-prog](https://github.com/psu-prog) in #1937. Fixed by [@Nacl-HCl](https://github.com/Nacl-HCl).
1112
- `DisposeComponentsAsync()` could dispose nothing when the renderer's dispatcher was busy, e.g. with a timer calling `InvokeAsync`. Reported by [@xfunc-tim-schneider](https://github.com/xfunc-tim-schneider) in #1939. Fixed by [@linkdotnet](https://github.com/linkdotnet).
1213
- Markup generation and `FindComponents` could recurse until the test host crashed with a stack overflow when a render threw while instantiating a child component. Reported in #1935. Fixed by [@linkdotnet](https://github.com/linkdotnet).
1314
- `Find()` could keep returning DOM nodes from the previous render when a render, e.g. from a timer, happened while the nodes were parsed, causing `UnknownEventHandlerIdException`. Reported by [@xfunc-tim-schneider](https://github.com/xfunc-tim-schneider) in #1938. Fixed by [@linkdotnet](https://github.com/linkdotnet).

‎src/bunit.generators.internal/Web.AngleSharp/WrapperBase.cs‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,7 @@ public abstract class WrapperBase<TElement> : IElementWrapper<TElement>
2424
public TElement WrappedElement
2525
{
2626
[MethodImpl(MethodImplOptions.AggressiveInlining)]
27-
get
28-
{
29-
if (element is null)
30-
element = elementFactory.GetElement<TElement>();
31-
32-
return element;
33-
}
27+
get => element ??= elementFactory.GetElement<TElement>();
3428
}
3529

3630
/// <summary>

‎tests/bunit.generators.tests/Web.AngleSharp/WrapperElementsGeneratorTest.Generator#WrapperBase.g.verified.cs‎

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//HintName: WrapperBase.g.cs
1+
//HintName: WrapperBase.g.cs
22
#nullable enable
33
using System.CodeDom.Compiler;
44
using System.Diagnostics;
@@ -25,13 +25,7 @@ public abstract class WrapperBase<TElement> : IElementWrapper<TElement>
2525
public TElement WrappedElement
2626
{
2727
[MethodImpl(MethodImplOptions.AggressiveInlining)]
28-
get
29-
{
30-
if (element is null)
31-
element = elementFactory.GetElement<TElement>();
32-
33-
return element;
34-
}
28+
get => element ??= elementFactory.GetElement<TElement>();
3529
}
3630

3731
/// <summary>

‎tests/bunit.tests/Extensions/RefreshingWrappedElementTest.cs‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,53 @@ public void Test031()
7676
Should.Throw<ElementRemovedFromDomException>(() => btn.TextContent);
7777
}
7878

79+
[Fact(DisplayName = "Found element is never null when accessed concurrently with component re-renders")]
80+
public async Task Test040()
81+
{
82+
var cut = Render<Counter>();
83+
var elm = cut.Find("span");
84+
using var cts = CancellationTokenSource.CreateLinkedTokenSource(Xunit.TestContext.Current.CancellationToken);
85+
cts.CancelAfter(TimeSpan.FromSeconds(1));
86+
var renders = Task.Run(async () =>
87+
{
88+
while (!cts.IsCancellationRequested)
89+
{
90+
await cut.InvokeAsync(cut.Instance.Increment);
91+
}
92+
}, Xunit.TestContext.Current.CancellationToken);
93+
94+
try
95+
{
96+
while (!cts.IsCancellationRequested)
97+
{
98+
elm.TextContent.ShouldNotBeNull();
99+
}
100+
}
101+
finally
102+
{
103+
await cts.CancelAsync();
104+
await renders;
105+
}
106+
}
107+
108+
private sealed class Counter : ComponentBase
109+
{
110+
private int count;
111+
112+
public void Increment()
113+
{
114+
count++;
115+
StateHasChanged();
116+
}
117+
118+
protected override void BuildRenderTree(RenderTreeBuilder builder)
119+
{
120+
builder.OpenElement(0, "span");
121+
builder.AddContent(1, count);
122+
builder.CloseElement();
123+
}
124+
}
125+
79126
private sealed class Markup : ComponentBase
80127
{
81128
[Parameter] public string Base { get; set; } = string.Empty;

0 commit comments

Comments
 (0)