fix(suspension): keep the suspension driver and pending load per host… · reactiveui/ReactiveUI@de754f8 · GitHub
Skip to content

Commit de754f8

Browse files
authored
fix(suspension): keep the suspension driver and pending load per host (#4483)
## Summary **`SetupDefaultSuspendResume` uses the driver passed to it, and each suspension host loads its own state.** - **Each setup call keeps its own driver.** A later call with a different explicit driver is no longer ignored in favour of the first driver the process resolved. - **Each host keeps its own pending load.** Setting up a second host no longer replaces or consumes the first host's load, so `GetAppState` on the first host returns its state instead of `null`. ## Why **The driver and the one-time load lived in static fields shared by every host and every `TAppState`.** - The first resolved driver won for the rest of the process, so saves and loads went to the wrong driver. - The pending load was overwritten by each setup, so one host could run or lose another host's load. Closes #4479 ## Breaking changes **None.** - `GetAppState` on a host that was never set up no longer runs another host's pending load. ## How this was verified **Tests cover two hosts with two different drivers on both the typed (`JsonTypeInfo`) and untyped paths, and a host that keeps loading through its setup driver after the service locator loses its drivers.** ## Notes for the reviewer **Start in `SuspensionHostExtensions.cs`.** - Each host's pending load is held in a `ConditionalWeakTable<ISuspensionHost, PendingLoad>`, so the table does not keep hosts alive; the load runs once through `Interlocked.Exchange`. - The internal test hooks for the removed static fields are gone, and `SuspensionHostTestExecutor` no longer saves and restores them. - Two existing tests asserted the shared-state behaviour and were rewritten to assert the per-host behaviour. ## Checklist - [x] I have read the [Contribute guide](https://www.reactiveui.net/contribute/index.html) - [x] The PR title follows [Conventional Commits](https://www.conventionalcommits.org/) - [x] Tests cover this change, or the summary says why they do not - [x] New or changed public API has XML documentation
1 parent 2a59e1d commit de754f8

5 files changed

Lines changed: 152 additions & 127 deletions

File tree

‎src/ReactiveUI.Shared/Suspension/SuspensionHostExtensions.cs‎

Lines changed: 54 additions & 60 deletions

‎src/tests/ReactiveUI.Test.Utilities/SuspensionHost/SuspensionHostTestExecutor.cs‎

Lines changed: 4 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,60 +5,11 @@
55

66
using ReactiveUI.Tests.Utilities.AppBuilder;
77

8-
#if REACTIVE_SHIM
9-
using ISuspensionDriverContract = ReactiveUI.Reactive.ISuspensionDriver;
10-
#else
11-
using ISuspensionDriverContract = ReactiveUI.ISuspensionDriver;
12-
#endif
13-
148
namespace ReactiveUI.Tests.Utilities.SuspensionHost;
159

16-
/// <summary>
17-
/// Test executor that manages SuspensionHostExtensions static state for test isolation.
18-
/// Saves and restores static fields before and after each test to prevent state leakage.
19-
/// </summary>
10+
/// <summary>Test executor for SuspensionHostExtensions tests.</summary>
2011
/// <remarks>
21-
/// This executor manages:
22-
/// - SuspensionHostExtensions.EnsureLoadAppStateFunc
23-
/// - SuspensionHostExtensions.SuspensionDriver
24-
/// Tests using this executor should be marked with [NotInParallel] due to static state modifications.
12+
/// SuspensionHostExtensions keeps its suspend/resume state per host, so each test isolates itself by creating its own
13+
/// host. This executor only adds the AppBuilder isolation that the tests need for the service locator and logging.
2514
/// </remarks>
26-
public class SuspensionHostTestExecutor : AppBuilderTestExecutor
27-
{
28-
/// <summary>The load-app-state func captured before the test, restored during cleanup.</summary>
29-
private Func<IObservable<RxVoid>>? _previousEnsureLoadAppStateFunc;
30-
31-
/// <summary>The suspension driver captured before the test, restored during cleanup.</summary>
32-
private ISuspensionDriverContract? _previousSuspensionDriver;
33-
34-
/// <inheritdoc/>
35-
public override async ValueTask ExecuteTest(TestContext context, Func<ValueTask> action)
36-
{
37-
ArgumentNullException.ThrowIfNull(action);
38-
39-
SaveStaticState();
40-
41-
try
42-
{
43-
await base.ExecuteTest(context, action);
44-
}
45-
finally
46-
{
47-
RestoreStaticState();
48-
}
49-
}
50-
51-
/// <summary>Saves the current static state from SuspensionHostExtensions.</summary>
52-
protected virtual void SaveStaticState()
53-
{
54-
_previousEnsureLoadAppStateFunc = SuspensionHostExtensions.EnsureLoadAppStateFunc;
55-
_previousSuspensionDriver = SuspensionHostExtensions.SuspensionDriver;
56-
}
57-
58-
/// <summary>Restores the previously saved static state to SuspensionHostExtensions.</summary>
59-
protected virtual void RestoreStaticState()
60-
{
61-
SuspensionHostExtensions.EnsureLoadAppStateFunc = _previousEnsureLoadAppStateFunc;
62-
SuspensionHostExtensions.SuspensionDriver = _previousSuspensionDriver;
63-
}
64-
}
15+
public class SuspensionHostTestExecutor : AppBuilderTestExecutor;

‎src/tests/ReactiveUI.Tests/Suspension/SuspensionHostExtensionsAotTests.StateLifecycle.cs‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,10 @@ public async Task EnsureLoadAppState_Typed_WithNullCreateNewAppState_SetsStateTo
231231
await Assert.That((object?)state).IsNull();
232232
}
233233

234-
/// <summary>Verifies the typed EnsureLoadAppState logs an error and leaves state null when the driver becomes null.</summary>
234+
/// <summary>Verifies the typed EnsureLoadAppState still loads through the setup driver after the locator loses its drivers.</summary>
235235
/// <returns>A <see cref="Task" /> representing the asynchronous operation.</returns>
236236
[Test]
237-
public async Task EnsureLoadAppState_Typed_DriverBecomesNull_LogsErrorAndStateRemainsNull()
237+
public async Task EnsureLoadAppState_Typed_LocatorDriversRemovedAfterSetup_LoadsThroughSetupDriver()
238238
{
239239
using var host = new SuspensionHost<TestAppState>
240240
{
@@ -253,11 +253,10 @@ public async Task EnsureLoadAppState_Typed_DriverBecomesNull_LogsErrorAndStateRe
253253
Splat.Locator.CurrentMutable.UnregisterAll<ISuspensionDriver>();
254254
try
255255
{
256-
SuspensionHostExtensions.SuspensionDriver = null;
257-
258256
var state = host.GetAppState();
259257

260-
await Assert.That((object?)state).IsNull();
258+
await Assert.That(state).IsSameReferenceAs(driver.StateToLoad);
259+
await Assert.That(driver.LoadStateCallCount).IsEqualTo(1);
261260
}
262261
finally
263262
{

‎src/tests/ReactiveUI.Tests/Suspension/SuspensionHostExtensionsAotTests.cs‎

Lines changed: 43 additions & 0 deletions

0 commit comments

Comments
 (0)