-
-
Notifications
You must be signed in to change notification settings - Fork 109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
FragmentContainer was not found in async test #1143
Comments
Ok, I don't completely understand this (since the code ought to be equivalent, with the exception of protected override void OnParametersSet()
{
var task = Task;
if (task != _RegisteredTask)
{
_RegisteredTask = task;
task?.GetAwaiter().OnCompleted(() =>
{
if (task == Task)
{
StateHasChanged();
}
});
}
base.OnParametersSet();
} So, yay I guess? This version of the MCVE code brings back the exceptions: private Task<object?>? _WrappedTask;
protected override void OnParametersSet()
{
var task = Task;
if (task != _RegisteredTask)
{
_RegisteredTask = task;
_WrappedTask = task == null ? null : Wrap(task);
var localTask = _WrappedTask;
localTask?.ConfigureAwait(false).GetAwaiter().OnCompleted(() =>
{
if (localTask == _WrappedTask)
{
_ = InvokeAsync(StateHasChanged);
}
});
}
base.OnParametersSet();
}
private static async Task<object?> Wrap(Task task)
{
await task;
return null;
} Amending |
Thanks for reporting this @uecasm. Do you mind trying this with the latest release? |
1.21.9 is the latest release, isn't it? At least it's the newest on nuget.org... |
I found an intersting issue on the ASP.NET Core GitHub Tracker that has a similar problem: dotnet/aspnetcore#43364 That would to some extend explain some of the phenomena you discovered. As said note: The Blazor team encourages not to use |
Did the latest release fix the issue? |
No, I think you misunderstood. This was the latest release the whole time, at least for the MCVE (see the bottom of the original post). |
I tried and was able to reproduce using NUnit, but not XUnit.: This is my test case: @inherits TestContext
@code {
#if NET6_0_OR_GREATER
[NUnit.Framework.Test()]
[NUnit.Framework.Repeat(1000)]
public void CancelNUnit()
{
using var ctx = new TestContext();
var tcs = new TaskCompletionSource();
using var cut = ctx.Render(@<InvokeAsyncInsideContinueWith Task="@tcs.Task"/>);
cut.MarkupMatches(@<span>waiting</span>);
tcs.SetCanceled();
// The exception is being thrown when the render fragment passed to MarkupMatches
// is rendered before getting passed to the diffing algorithm.
cut.WaitForAssertion(() => cut.MarkupMatches(@<span>done</span>));
}
[Fact]
[Repeat(1000)]
public void CancelXunit()
{
var tcs = new TaskCompletionSource();
using var cut = Render(@<InvokeAsyncInsideContinueWith Task="@tcs.Task"/>);
cut.MarkupMatches(@<span>waiting</span>);
tcs.SetCanceled();
cut.WaitForAssertion(() => cut.MarkupMatches(@<span>done</span>));
}
#endif
} Full exception output:
I am still not sure why this happens. The Will continue to investigate. @linkdotnet if you can take a look too it would be very helpful. |
I did run your test 25k times without any issue - how does your Edit: I did run the test on the current |
My
|
Okay - I used the same and for me it doesn't fail - even after 100000 runs |
Pushed branch |
Interesting - on your branch it fails for me but when I locally did the same it did not. |
It only happens with razor syntax. The error happens when MarkupMatches converts the render fragment to markup. I also saw the xunit test fail now, so it does not matter that NUnit doesn't use a sync context, it seems. |
I guess I am stating some obvious stuff, but nevertheless: cut.WaitForAssertion(() => cut.MarkupMatches("<span>done</span>")); // Works
cut.WaitForAssertion(() => cut.MarkupMatches(@<span>done</span>)); // Doesn't work The obvious difference is that the second one, the one that does accept a public static void MarkupMatches(this IRenderedFragment actual, RenderFragment expected, string? userMessage = null)
{
// ....
var testContext = actual.Services.GetRequiredService<TestContextBase>();
var renderedFragment = (IRenderedFragment)testContext.RenderInsideRenderTree(expected);
MarkupMatches(actual, renderedFragment, userMessage);
} So what follows is that the |
That is my guess too. In addition, I guess that the render of the markup matches fragment actually isn't able to run (renderer is locked) because the async triggered render by the TCS is blocking the renderer because it is rendering. |
As way forward #1018 becomes a valid candidate - when |
Yes and no. This behavior could indicate that an assumption bUnit has is not correct, so I do want to understand this fully. |
FWIW after I changed my real app to use the latest MCVE version ( |
If you want to eliminate it completely (until (if) we find a fix), don't pass a
|
@linkdotnet I am pretty certain I understand what is going on. Here is my thinking:
This seems unique to using a rendering a render fragment in a WaitForX checker. Doing other things like calling UPDATE confirmed by logging this in the renderer when Suggestions?
|
Fund the assumption that we have had that didn't hold: bUnit/src/bunit.core/Rendering/TestRenderer.cs Lines 379 to 382 in 674a555
|
Hmmm my way of thinking is that we have the dispatcher before, which result we for sure await ( |
Here is all the code of the bUnit/src/bunit.core/Rendering/TestRenderer.cs Lines 349 to 389 in 674a555
Even after the renderTask is completed there is still a chance that the root component has not rendered yet. If you download the |
Correction, no other tests are failing, if I move The CancelXunit tests do fail though with the |
We could instantiate our own renderer-instance for |
That will likely work. Should we attempt to reuse renderer instances if they are not currently blocked? What about Services? We register a renderer in there that is getting pulled out in certain circumstances, should that be a transient registration instead? |
Probably newing up a renderer might be "good enough". The average use case is not the pass in another Blazor component that needs registered services and has async lifecycle. If the assumption doesn't hold true, we still can get the DI container from EDIT: As the extension method and renderer life in different assemblies, it will get rather complex to grab all the information necessary. |
It would definitely need to make services from the Granted, I don't think I currently have any tests comparing against components that have complex service dependencies (and certainly not anything async), but it wouldn't surprise me if someone does, even if only a logger. It does make sense to me for the |
Making it transient should work but might have side effects when you use render or findcomponent while there might be still ongoing async operations. So I would refrain from doing that for now |
Thanks for the input folks. @uecasm if you need to compare with the output of a RenderFragment, you could do the following: @using NUnit.Framework
@using Bunit
@*@inherits Bunit.TestContext*@
@inherits BunitTestContext /* this uses TestContextWrapper */
@code {
[Test]
public void Cancel1()
{
var tcs = new TaskCompletionSource();
var expectedMarkup = Render(@<span>done</span>);
var cut = Render(@<MyComponent Task="@tcs.Task"/>);
cut.MarkupMatches(@<span>waiting</span>);
tcs.SetCanceled();
cut.WaitForAssertion(() => cut.MarkupMatches(expectedMarkup.Markup));
}
} |
I try to find some time to provide a fix |
@egil I pushed a commit on your branch that fixes the test in question - it isn't the cleanest solution, but I guess this is a way forward. Let me know your thoughts and I spent some time to make it nice |
@uecasm can you try again with the 1.22.16-preview release and see if that solves the issue for you. |
The MCVE code using Same when using I'm not really seeing any difference between the two versions. Just for the sake of only- protected override void OnParametersSet()
{
var task = Task;
if (task != _RegisteredTask)
{
_RegisteredTask = task;
_DelegatedTask = task == null ? null : DelegateTo(task);
RenderWhenDone();
}
base.OnParametersSet();
}
private async void RenderWhenDone()
{
var task = _DelegatedTask;
if (task != null)
{
_ = await Task.WhenAny(task).ConfigureAwait(false);
if (task == _DelegatedTask)
{
_ = InvokeAsync(StateHasChanged);
}
}
}
private static async Task<object?> DelegateTo(Task task)
{
await task;//.ConfigureAwait(false);
return null;
} This fails (in both versions). If the |
@uecasm just to double check. Does the test fail with the "FragmentContainer was not found" error or does the test just fail its assertion? |
Yes, it's the same FragmentContainer exception. |
With the latest version, this component: @if (Task != null)
{
@if (Task.IsCompleted)
{
<span>done</span>
}
else
{
<span>waiting</span>
}
}
@code {
[Parameter] public Task? Task { get; set; }
private Task? registeredTask;
protected override void OnParametersSet()
{
var task = Task;
if (task != registeredTask)
{
registeredTask = task;
_ = task?.ContinueWith((t, o) =>
{
if (t == Task)
{
_ = InvokeAsync(StateHasChanged);
}
}, TaskScheduler.FromCurrentSynchronizationContext());
}
base.OnParametersSet();
}
} Passes the following test: [Fact]
public void MarkupMatchesShouldNotBeBlockedByRendererComplex()
{
var tcs = new TaskCompletionSource<object?>();
var cut = Render(@<InvokeAsyncInsideContinueWith Task="@tcs.Task"/> );
cut.MarkupMatches(@<span>waiting</span>);
tcs.SetResult(true);
cut.WaitForAssertion(() => cut.MarkupMatches(@<span>done</span>));
} Even the more complex version: @if (Task != null)
{
@if (Task.IsCompleted)
{
<span>done</span>
}
else
{
<span>waiting</span>
}
}
@code {
[Parameter] public Task? Task { get; set; }
private Task? registeredTask;
private Task? delegatedTask;
protected override void OnParametersSet()
{
var task = Task;
if (task != registeredTask)
{
registeredTask = task;
delegatedTask = task == null ? null : DelegateTo(task);
RenderWhenDone();
}
base.OnParametersSet();
}
private async void RenderWhenDone()
{
var task = delegatedTask;
if (task != null)
{
_ = await Task.WhenAny(task).ConfigureAwait(false);
if (task == delegatedTask)
{
_ = InvokeAsync(StateHasChanged);
}
}
}
private static async Task<object?> DelegateTo(Task task)
{
await task;//.ConfigureAwait(false);
return null;
}
} Passes - I am a bit puzzled what is going on. @uecasm can you check whether or not some cached bUnit version was taken? |
Are you running the test more than once in a single session? Note the use of three identical test cases in the original post. The first test always passes; it's only the second and third that fail. Also, while it shouldn't matter, I'm using For reference, here's the exception trace from the preview build:
On the upside, the workaround of pre-rendering the expected content beforehand does do the trick; I haven't had any failures with that regardless of where the |
@uecasm there should be a new pre-release available on NuGet: 1.22.18-preview |
Preview 18 does indeed seem to pass the MCVE, in several attempts and variations. (Though it still fails #1159.) |
Describe the bug
I have a relatively simple component that just renders different content depending on the state of a
Task
. The code of both component and test is very similar to the async example except that instead of awaiting the task on init I'm usingtask.ContinueWith(...) => InvokeAsync(StateHasChanged)
and using the razor test syntax.When using bUnit 1.16.2 and not using
WaitForAssertion
, the tests almost always pass. (I did very very rarely observe the samewaiting
failure as below.)When using later versions of bUnit, the tests will more frequently intermittently fail (showing the
waiting
content rather than thedone
content).When I tried adding
WaitForAssertion
(in 1.16.2) it started instead failing with:I haven't been able to replicate precisely this behaviour in a MCVE test, but what I did manage to reproduce is described below.
(Oddly, the MCVE code always fails (with
waiting
content, not the exception) when not usingWaitForAssertion
. While not exactly surprising due to async, it's odd that it's different; though it's likely that this is due to the real component being a bit more complex.)Example:
Testing this component:
With this test:
(Note that this is three identical copies of the same test.)
Expected behavior:
All tests should pass.
Actual behavior:
The first test always passes. The other two tests intermittently fail with the exception above.
If I run the tests in the debugger (without stopping on any breakpoints or exceptions), all tests pass.
Version info:
The text was updated successfully, but these errors were encountered: