-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Description
Goal: Make tests deterministic. Audit tests for usage of static/global state and ensure each such test:
- does not run in parallel, and
- restores all mutated static state to its prior values (via helper scopes/fixtures as needed).
This work is blocked unless the build & test environment is set up exactly as specified below. Follow the pre-flight steps first.
🔒 Pre-flight: Build Environment Requirements (must be completed before any code changes)
Cloning the Repository
CRITICAL: Perform a full, recursive clone. A shallow clone (--depth 1
) will fail because the build reads git version info.
# Full recursive clone
git clone --recursive https://github.com/reactiveui/reactiveui.git
cd reactiveui
Required Tools
- .NET SDKs: 8.0, 9.0, and 10.0
CRITICAL: Install all three SDKs via the official script (this can work on LINUX, do not bother checking the current versions install LATEST REGARDLESS, DO THIS FIRST, FIRST, FIRST BEFORE DOING ANYTHING ELSE)
# Download the installation script
Invoke-WebRequest -Uri https://dot.net/v1/dotnet-install.ps1 -OutFile dotnet-install.ps1
# Install .NET 8 SDK
./dotnet-install.ps1 -Channel 8.0 -InstallDir ./.dotnet
# Install .NET 9 SDK
./dotnet-install.ps1 -Channel 9.0 -InstallDir ./.dotnet
# Install .NET 10 SDK
./dotnet-install.ps1 -Channel 10.0 -InstallDir ./.dotnet
(Tip: ensure ./.dotnet
is on PATH for this shell session, or call it explicitly via ./.dotnet/dotnet
.)
Solution Files
- Main solution:
src/ReactiveUI.sln
- Integration test solutions live under
integrationtests/
(not required for this task)
🛠️ Build & Test Commands (Windows PowerShell or CMD, repository root)
CRITICAL: dotnet workload restore
must be run from /src
before building.
dotnet --info
# CRITICAL: restore workloads from /src
cd src
dotnet workload restore
cd ..
# Restore packages
dotnet restore src/ReactiveUI.sln
# Build (requires Windows for platform targets)
dotnet build src/ReactiveUI.sln -c Release -warnaserror
# Run tests (includes AOT tests requiring .NET 9.0)
dotnet test src/ReactiveUI.sln -c Release --no-build
Note: Building on Linux/macOS will fail due to net*-windows
targets and AOT tests. Use Windows.
🧭 Context & Example Failure
We’re seeing intermittent failures tied to static/global state. Example:
Failed AutoPersistDoesntWorkOnNonDataContractClasses
ReactiveUI.UnhandledErrorException ... Could not find a ICreatesObservableForProperty ...
... your service locator is probably broken ...
at ReactiveUI.RxApp.<>c__DisplayClass9_0.<.cctor>b__2() ...
This indicates tests mutate or rely on global/static state (e.g., RxApp
, service locator, schedulers, message bus, etc.) without restoring it. Running in parallel amplifies the flakiness.
✅ What to do
1) Discover tests that touch static/global state
Search for usages of well-known static entry points or singletons anywhere under src/ReactiveUI.Tests
:
- ReactiveUI:
RxApp.
,MessageBus.
,SuspensionHost.
,Interaction.
,ModeDetector.
,LogHost.
- Splat / DI:
Locator.
,Locator.Current
,Locator.CurrentMutable
,DependencyResolver
,Splat.Locator
- Any other
static
members that mutate shared state (caches, registries, global flags, default schedulers, etc.)
Suggested queries (run at repo root):
git grep -n "RxApp\." -- src/ReactiveUI.Tests
git grep -n "Locator\." -- src/ReactiveUI.Tests
git grep -n "MessageBus\." -- src/ReactiveUI.Tests
git grep -n "SuspensionHost\." -- src/ReactiveUI.Tests
git grep -n "ModeDetector\." -- src/ReactiveUI.Tests
git grep -n "LogHost\." -- src/ReactiveUI.Tests
git grep -n "static " -- src/ReactiveUI.Tests
Output: Build a checklist of test files and specific tests that read or write static/global state.
2) Make affected tests non-parallelizable (targeted)
For each test fixture or test that touches static/global state, explicitly disable parallelization with NUnit:
-
At the fixture level (preferred when multiple tests in a class touch static state):
using NUnit.Framework; [TestFixture] [NonParallelizable] // or [Parallelizable(ParallelScope.None)] public class FooTests { ... }
-
Or at the individual test level if only one or two tests are affected:
[Test, NonParallelizable] public void MyTest() { ... }
Do not disable parallelization assembly-wide. Keep the scope as tight as possible.
3) Introduce helper scopes to snapshot & restore static state
Create small, focused disposable scopes for each area of static state. These should snapshot on construction, and restore on Dispose()
. Use them in SetUp
/TearDown
or in using
blocks inside tests.
⚠️ Use the actual APIs present in this codebase—names below are examples. Inspect the concrete properties/methods onRxApp
,Locator
, etc., before implementing.
Examples (sketches):
// Example: RxApp schedulers
sealed class RxAppSchedulersScope : IDisposable
{
private readonly IScheduler _main;
private readonly IScheduler _taskpool;
public RxAppSchedulersScope()
{
_main = RxApp.MainThreadScheduler;
_taskpool = RxApp.TaskpoolScheduler;
// If tests override them, do it after taking the snapshot
}
public void Dispose()
{
RxApp.MainThreadScheduler = _main;
RxApp.TaskpoolScheduler = _taskpool;
}
}
// Example: Service locator / DI container (Splat)
sealed class LocatorScope : IDisposable
{
private readonly IMutableDependencyResolver _snapshotResolver;
public LocatorScope()
{
// Take a deep copy or create a new resolver seeded from current,
// depending on what's available in this codebase.
// If Splat exposes a "GetResolverSnapshot()" or similar, prefer that.
_snapshotResolver = Locator.CurrentMutable; // placeholder — replace with proper snapshot strategy
}
public void Dispose()
{
// Restore the previous resolver/snapshot with the correct API.
// E.g., Locator.SetLocator(_snapshotResolver) or equivalent.
}
}
// Example: MessageBus
sealed class MessageBusScope : IDisposable
{
private readonly IMessageBus _prev;
public MessageBusScope()
{
_prev = MessageBus.Current; // if available in this codebase
}
public void Dispose()
{
MessageBus.Current = _prev; // restore via actual API
}
}
Usage in NUnit fixtures:
[TestFixture]
[NonParallelizable]
public class MyStaticStateTests
{
private RxAppSchedulersScope _schedulersScope;
private LocatorScope _locatorScope;
private MessageBusScope _busScope;
[SetUp]
public void SetUp()
{
_schedulersScope = new RxAppSchedulersScope();
_locatorScope = new LocatorScope();
_busScope = new MessageBusScope();
// If the test needs to override defaults, do it here
// e.g., RxApp.MainThreadScheduler = Scheduler.Immediate;
}
[TearDown]
public void TearDown()
{
_busScope?.Dispose();
_locatorScope?.Dispose();
_schedulersScope?.Dispose();
}
[Test]
public void Something() { /* ... */ }
}
If a “true” snapshot API does not exist for a given static (e.g., DI container), implement a pragmatic approach:
- Capture the current instance/reference, or
- Instantiate a fresh isolated instance and restore the original instance afterward, or
- Provide a factory that yields a throwaway container per test and redirects global resolve calls during the scope.
Add helper scopes under src/ReactiveUI.Tests/Infrastructure/StaticState/
(or a similar shared test folder) with clear names and XML docs.
4) Fix tests that subscribe to erroring observable pipelines
Where relevant (e.g., ReactiveCommand
, ObservableAsPropertyHelper
), tests should subscribe to ThrownExceptions
or ensure the pipeline cannot error. If a test intentionally drives an error, assert it via ThrownExceptions
and ensure the global unhandled error path does not leak between tests.
5) Keep changes tight and mechanical
- Only mark fixtures/tests as non-parallelizable when they do touch static/global state.
- Prefer reusable helper scopes over ad-hoc resets in each test.
- Do not weaken test assertions; only improve isolation and determinism.
📋 Acceptance Criteria
- All environment steps completed exactly as specified (recursive clone; .NET 8/9/10 installed;
dotnet workload restore
run from/src
). -
dotnet build src/ReactiveUI.sln -c Release
succeeds on Windows. -
dotnet test src/ReactiveUI.sln -c Release --no-build
runs passing and deterministically (no flickers across multiple runs). - A checklist (in this issue or PR description) of test files updated, including which static/global areas they touched.
- New
StaticState*Scope
helper(s) added with unit tests covering “snapshot → mutate → restore”. - Affected test fixtures/tests annotated with
[NonParallelizable]
(or[Parallelizable(ParallelScope.None)]
) and documented rationale. - No assembly-wide disabling of parallelism; unaffected tests continue to run in parallel.
🧩 Nice-to-have (optional)
- A small analyzer/script (even a
git grep
-backed tool) to surface static/global usages inReactiveUI.Tests
, producing a list for future contributors. - A shared
TestBase
that wires common scopes inSetUp
/TearDown
for teams to opt into.
🔎 Notes for the failure shown in logs
The UnhandledErrorException
and “service locator is probably broken” error strongly suggest cross-test interference of DI/service locator and/or RxApp scheduler state (e.g., from HostTestFixture
). Ensure those fixtures:
- are non-parallelizable, and
- snapshot/restore the locator and any RxApp static configuration.
PR Checklist (fill before request for review)
- Environment steps followed as written (paste
dotnet --info
top lines in PR description). - Summary of tests modified and why.
- Evidence of re-running tests multiple times with no flickers (e.g., 3 consecutive runs).
- Scope helpers and docs included.
Reminder: This task cannot be completed unless the environment steps at the top are followed exactly (recursive clone, all three .NET SDKs installed, and workloads restored from /src
).