diff --git a/CLAUDE.md b/CLAUDE.md index e507bc1cb1..e5eb81f270 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,55 +74,6 @@ Building the full solution requires **Windows** due to Windows-only target frame --- -## Running Windows/WPF Tests on Linux via Wine - -The Windows-only test projects (`ReactiveUI.Wpf.Tests`, `ReactiveUI.WinForms.Tests`) can be **built and run on Linux** through Wine, giving a fast local feedback loop without a Windows VM or waiting on CI. This works because the Windows targeting/runtime packs restore cross-platform (`EnableWindowsTargeting=true`) and Wine can host the .NET Desktop runtime. - -> Treat results as a strong signal, not gospel: this is Wine, not Windows. **CI on `windows-latest` is authoritative.** A green run here is high-confidence; investigate a red one before assuming a product bug. -> -> **Known Wine limitation — dispatcher thread-affinity:** Wine does **not** enforce WPF `Dispatcher` thread affinity the way Windows does, so `DispatcherObject.CheckAccess()` can return `true` on a non-dispatcher thread. Tests that marshal work from a background thread (e.g. `*FromBackgroundThread*`, anything asserting `Dispatcher.BeginInvoke`/scheduler hand-off after `DispatcherUtilities.DoEvents()`) may behave differently under Wine than on Windows — a Wine pass or fail for those is **not** conclusive. Verify background-thread/marshalling tests on CI. Wine is reliable for the large majority of WPF tests that run on a single (dispatcher) thread. - -### 1. One-time runtime + Wine prefix setup - -Assemble a Windows .NET Desktop runtime (base runtime gives `dotnet.exe` + host/fxr + `Microsoft.NETCore.App`; the desktop pack adds `Microsoft.WindowsDesktop.App` for WPF/WinForms). Match the version to the net8 windows TFM (bump as the SDK moves): - -```bash -VER=8.0.27 -mkdir -p ~/wine-dotnet8 && cd ~/wine-dotnet8 -curl -fsSL -o /tmp/dnr.zip https://builds.dotnet.microsoft.com/dotnet/Runtime/$VER/dotnet-runtime-$VER-win-x64.zip -curl -fsSL -o /tmp/wdr.zip https://builds.dotnet.microsoft.com/dotnet/WindowsDesktop/$VER/windowsdesktop-runtime-$VER-win-x64.zip -unzip -oq /tmp/dnr.zip # dotnet.exe + host/ + shared/Microsoft.NETCore.App -unzip -oq /tmp/wdr.zip # + shared/Microsoft.WindowsDesktop.App - -export WINEPREFIX=~/.wine-rxui WINEARCH=win64 -wineboot -i -wine ~/wine-dotnet8/dotnet.exe --list-runtimes # must list NETCore.App AND WindowsDesktop.App -``` - -### 2. Build the Windows-TFM test assembly (on Linux) - -The UI test TFMs are gated to Windows in `Directory.Build.props` (`ReactiveUITestingUITargets`), so force a single Windows TFM with a global property. **Clear `obj`/`bin` first** — stale non-Windows assets break the WPF `_wpftmp` markup pass with `NETSDK1005`: - -```bash -cd src -rm -rf tests/ReactiveUI.Wpf.Tests/obj tests/ReactiveUI.Wpf.Tests/bin -dotnet build tests/ReactiveUI.Wpf.Tests/ReactiveUI.Wpf.Tests.csproj -c Release \ - -p:ReactiveUITestingUITargets=net8.0-windows10.0.19041.0 -p:CheckEolTargetFramework=false -``` - -### 3. Run under Wine (MTP + TUnit `--treenode-filter`) - -```bash -cd src/tests/ReactiveUI.Wpf.Tests/bin/Release/net8.0-windows10.0.19041.0 -export WINEPREFIX=~/.wine-rxui WINEARCH=win64 WINEDEBUG=-all -wine ~/wine-dotnet8/dotnet.exe ReactiveUI.Wpf.Tests.dll \ - --treenode-filter "/*/*/*/ViewModelToViewBindingFromBackgroundThreadDoesNotTouchWpfControlDirectly" -``` - -`WINEDEBUG=-all` silences `fixme:`/`err:` chatter; pipe through `grep -viE 'fixme|^err:|wine:'` if needed. WinForms tests work identically (`ReactiveUI.WinForms.Tests`, same TFM override). - ---- - ## Testing: Microsoft Testing Platform (MTP) + TUnit This repo uses **Microsoft Testing Platform (MTP)** with **TUnit**. This differs from VSTest. diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 5fd5935ac9..63f2d952d1 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -7,10 +7,10 @@ 20.2.0 7.1.0 - 1.61.38 + 1.63.0 - 3.38.1 + 3.40.1 2.11.0.1 @@ -77,7 +77,7 @@ - + @@ -88,7 +88,7 @@ - + diff --git a/src/tests/ReactiveUI.AOTTests/AssemblyHooks.cs b/src/tests/ReactiveUI.AOTTests/AssemblyHooks.cs index 289a39b2b7..92b3f4a572 100644 --- a/src/tests/ReactiveUI.AOTTests/AssemblyHooks.cs +++ b/src/tests/ReactiveUI.AOTTests/AssemblyHooks.cs @@ -16,12 +16,12 @@ namespace ReactiveUI.AOT.Tests; public static class AssemblyHooks { /// Called before any tests in this assembly start. + /// + /// Overrides so the framework detects a unit test runner. + /// App builder initialization is handled per-test via AppBuilderTestExecutor. + /// [Before(Assembly)] - public static void AssemblySetup() => - - // Override ModeDetector to ensure we're detected as being in a unit test runner. - // App builder initialization is handled per-test via AppBuilderTestExecutor. - ModeDetector.OverrideModeDetector(new TestModeDetector()); + public static void AssemblySetup() => ModeDetector.OverrideModeDetector(new TestModeDetector()); /// Mode detector that always indicates we're in a unit test runner. private sealed class TestModeDetector : IModeDetector diff --git a/src/tests/ReactiveUI.Builder.Tests/AssemblyHooks.cs b/src/tests/ReactiveUI.Builder.Tests/AssemblyHooks.cs index bd7bdfec04..10569f709c 100644 --- a/src/tests/ReactiveUI.Builder.Tests/AssemblyHooks.cs +++ b/src/tests/ReactiveUI.Builder.Tests/AssemblyHooks.cs @@ -11,12 +11,12 @@ namespace ReactiveUI.Builder.Tests; public static class AssemblyHooks { /// Called before any tests in this assembly start. + /// + /// Overrides so the framework detects a unit test runner. + /// App builder initialization is handled per-test via AppBuilderTestExecutor. + /// [Before(Assembly)] - public static void AssemblySetup() => - - // Override ModeDetector to ensure we're detected as being in a unit test runner. - // App builder initialization is handled per-test via AppBuilderTestExecutor. - ModeDetector.OverrideModeDetector(new TestModeDetector()); + public static void AssemblySetup() => ModeDetector.OverrideModeDetector(new TestModeDetector()); /// Mode detector that always indicates we're in a unit test runner. private sealed class TestModeDetector : IModeDetector diff --git a/src/tests/ReactiveUI.Maui.Tests/AssemblyHooks.cs b/src/tests/ReactiveUI.Maui.Tests/AssemblyHooks.cs index 4fa840f495..cfded88c51 100644 --- a/src/tests/ReactiveUI.Maui.Tests/AssemblyHooks.cs +++ b/src/tests/ReactiveUI.Maui.Tests/AssemblyHooks.cs @@ -12,11 +12,9 @@ namespace ReactiveUI.Maui.Tests; public static class AssemblyHooks { /// Called before any tests in this assembly start. + /// Overrides so the framework detects a unit test runner. [Before(Assembly)] - public static void AssemblySetup() => - - // Override ModeDetector to ensure we're detected as being in a unit test runner - ModeDetector.OverrideModeDetector(new TestModeDetector()); + public static void AssemblySetup() => ModeDetector.OverrideModeDetector(new TestModeDetector()); /// Mode detector that always indicates we're in a unit test runner. private sealed class TestModeDetector : IModeDetector diff --git a/src/tests/ReactiveUI.Routing.Tests/AssemblyHooks.cs b/src/tests/ReactiveUI.Routing.Tests/AssemblyHooks.cs index d0e8e6c6fc..d0b5eb383a 100644 --- a/src/tests/ReactiveUI.Routing.Tests/AssemblyHooks.cs +++ b/src/tests/ReactiveUI.Routing.Tests/AssemblyHooks.cs @@ -16,14 +16,14 @@ namespace ReactiveUI.Tests; public static class AssemblyHooks { /// Called before any tests in this assembly start. + /// + /// Detects as a unit-test runner so platform registrations skip live scheduler wiring. + /// Per-test ReactiveUI builder initialization is handled by AppBuilderTestExecutor, which + /// the routing leaf otherwise lacked - without it the first WhenAny* call hits the + /// ReactiveNotifyPropertyChangedMixins static ctor and EnsureInitialized() throws. + /// [Before(Assembly)] - public static void AssemblySetup() => - - // Detect as a unit-test runner so platform registrations skip live scheduler wiring. - // Per-test ReactiveUI builder initialization is handled by AppBuilderTestExecutor, which - // the routing leaf otherwise lacked — without it the first WhenAny* call hits the - // ReactiveNotifyPropertyChangedMixins static ctor and EnsureInitialized() throws. - ModeDetector.OverrideModeDetector(new TestModeDetector()); + public static void AssemblySetup() => ModeDetector.OverrideModeDetector(new TestModeDetector()); /// Mode detector that always indicates we're in a unit test runner. private sealed class TestModeDetector : IModeDetector diff --git a/src/tests/ReactiveUI.Splat.Tests/AssemblyHooks.cs b/src/tests/ReactiveUI.Splat.Tests/AssemblyHooks.cs index 8552e3d7b4..ceadedce65 100644 --- a/src/tests/ReactiveUI.Splat.Tests/AssemblyHooks.cs +++ b/src/tests/ReactiveUI.Splat.Tests/AssemblyHooks.cs @@ -12,11 +12,9 @@ namespace ReactiveUI.Splat.Tests; public static class AssemblyHooks { /// Called before any tests in this assembly start. + /// Overrides so the framework detects a unit test runner. [Before(Assembly)] - public static void AssemblySetup() => - - // Override ModeDetector to ensure we're detected as being in a unit test runner - ModeDetector.OverrideModeDetector(new TestModeDetector()); + public static void AssemblySetup() => ModeDetector.OverrideModeDetector(new TestModeDetector()); /// Mode detector that always indicates we're in a unit test runner. private sealed class TestModeDetector : IModeDetector diff --git a/src/tests/ReactiveUI.TestGuiMocks/CommonGuiMocks/Mocks/RaceConditionFixture.cs b/src/tests/ReactiveUI.TestGuiMocks/CommonGuiMocks/Mocks/RaceConditionFixture.cs index 3d287336db..c5e5625989 100644 --- a/src/tests/ReactiveUI.TestGuiMocks/CommonGuiMocks/Mocks/RaceConditionFixture.cs +++ b/src/tests/ReactiveUI.TestGuiMocks/CommonGuiMocks/Mocks/RaceConditionFixture.cs @@ -13,18 +13,16 @@ public class RaceConditionFixture : ReactiveObject private readonly ObservableAsPropertyHelper _a; /// Initializes a new instance of the class. + /// + /// Emits a value on subscription that differs from the default, which triggers the property + /// change firing during the constructor - the race + /// this fixture exists to reproduce. + /// [System.Diagnostics.CodeAnalysis.SuppressMessage( "Design", "SST2403:'this' escapes before construction finishes", Justification = "canonical ObservableAsPropertyHelper initialization requires 'this' in the constructor; the single-threaded fixture never exposes the half-built instance.")] - public RaceConditionFixture() => - - // We need to generate a value on subscription - // which is different than the default value. - // This triggers the property change firing - // upon subscription in the ObservableAsPropertyHelper - // constructor. - Signal.Emit(true).Do(_ => Count++).ToProperty(this, x => x.A, out _a); + public RaceConditionFixture() => Signal.Emit(true).Do(_ => Count++).ToProperty(this, x => x.A, out _a); /// Gets or sets the count. public int Count { get; set; } diff --git a/src/tests/ReactiveUI.TestGuiMocks/CommonGuiMocks/Mocks/RaceConditionNameOfFixture.cs b/src/tests/ReactiveUI.TestGuiMocks/CommonGuiMocks/Mocks/RaceConditionNameOfFixture.cs index 8401e05faa..e034f9a8b9 100644 --- a/src/tests/ReactiveUI.TestGuiMocks/CommonGuiMocks/Mocks/RaceConditionNameOfFixture.cs +++ b/src/tests/ReactiveUI.TestGuiMocks/CommonGuiMocks/Mocks/RaceConditionNameOfFixture.cs @@ -12,17 +12,16 @@ public class RaceConditionNameOfFixture : ReactiveObject private readonly ObservableAsPropertyHelper _a; /// Initializes a new instance of the class. + /// + /// Emits a value on subscription that differs from the default, which triggers the property + /// change firing during the constructor - the race + /// this fixture exists to reproduce. + /// [System.Diagnostics.CodeAnalysis.SuppressMessage( "Design", "SST2403:'this' escapes before construction finishes", Justification = "canonical ObservableAsPropertyHelper initialization requires 'this' in the constructor; the single-threaded fixture never exposes the half-built instance.")] public RaceConditionNameOfFixture() => - - // We need to generate a value on subscription - // which is different than the default value. - // This triggers the property change firing - // upon subscription in the ObservableAsPropertyHelper - // constructor. Signal.Emit(true) .Do(_ => Count++) .ToProperty(this, nameof(A), out _a); diff --git a/src/tests/ReactiveUI.Testing.Tests/AppBuilderTestBaseTests.cs b/src/tests/ReactiveUI.Testing.Tests/AppBuilderTestBaseTests.cs index d498efcd90..4c594589f4 100644 --- a/src/tests/ReactiveUI.Testing.Tests/AppBuilderTestBaseTests.cs +++ b/src/tests/ReactiveUI.Testing.Tests/AppBuilderTestBaseTests.cs @@ -51,8 +51,6 @@ public async Task RunAppBuilderTestAsync_WithSyncTestBody_ExecutesTest() /// A representing the asynchronous unit test. [Test] public async Task RunAppBuilderTestAsync_WithAsyncTestBody_PropagatesExceptions() => - - // Act & Assert await Assert.That(static async () => { await TestHelper.RunAppBuilderTestAsync(static async () => @@ -66,8 +64,6 @@ await TestHelper.RunAppBuilderTestAsync(static async () => /// A representing the asynchronous unit test. [Test] public async Task RunAppBuilderTestAsync_WithSyncTestBody_PropagatesExceptions() => - - // Act & Assert await Assert.That(static async () => await TestHelper.RunAppBuilderTestAsync(static () => throw new InvalidOperationException("Test exception"))).Throws(); /// Verifies that can be called multiple times sequentially without interference. diff --git a/src/tests/ReactiveUI.Testing.Tests/AssemblyHooks.cs b/src/tests/ReactiveUI.Testing.Tests/AssemblyHooks.cs index f8bdbb7bc4..f46507eccb 100644 --- a/src/tests/ReactiveUI.Testing.Tests/AssemblyHooks.cs +++ b/src/tests/ReactiveUI.Testing.Tests/AssemblyHooks.cs @@ -17,12 +17,12 @@ namespace ReactiveUI.Testing.Tests; public static class AssemblyHooks { /// Called before any tests in this assembly start. + /// + /// Overrides so the framework detects a unit test runner. + /// App builder initialization is handled per-test via test executors. + /// [Before(Assembly)] - public static void AssemblySetup() => - - // Override ModeDetector to ensure we're detected as being in a unit test runner. - // App builder initialization is handled per-test via test executors. - ModeDetector.OverrideModeDetector(new TestModeDetector()); + public static void AssemblySetup() => ModeDetector.OverrideModeDetector(new TestModeDetector()); /// Mode detector that always indicates we're in a unit test runner. private sealed class TestModeDetector : IModeDetector diff --git a/src/tests/ReactiveUI.Testing.Tests/RxTestTests.cs b/src/tests/ReactiveUI.Testing.Tests/RxTestTests.cs index 637eb7b432..e0bbce38be 100644 --- a/src/tests/ReactiveUI.Testing.Tests/RxTestTests.cs +++ b/src/tests/ReactiveUI.Testing.Tests/RxTestTests.cs @@ -56,8 +56,6 @@ await Assert.That(static () => RxTest.AppBuilderTestAsync(null!)) /// Thrown if the delegate passed to AppBuilderTestAsync throws an InvalidOperationException. [Test] public async Task AppBuilderTestAsync_PropagatesExceptions() => - - // Act & Assert await Assert.That(static async () => await RxTest.AppBuilderTestAsync(static () => throw new InvalidOperationException("Test exception"))).Throws(); /// @@ -161,8 +159,6 @@ await RxTest.AppBuilderTestAsync( /// A task that represents the asynchronous test operation. [Test] public async Task AppBuilderTestAsync_ThrowsTimeoutException_WhenTestExceedsTimeout() => - - // Act & Assert await Assert.That(static async () => { await RxTest.AppBuilderTestAsync( @@ -180,8 +176,5 @@ static async () => /// A task that represents the asynchronous test operation. [Test] public Task AppBuilderTestAsync_HandlesTaskCompletedTask() => - - // This test verifies that returning Task.CompletedTask works correctly - // Act - Should not throw RxTest.AppBuilderTestAsync(static () => Task.CompletedTask); } diff --git a/src/tests/ReactiveUI.Tests/Bindings/Converters/ConverterMigrationHelperTests.cs b/src/tests/ReactiveUI.Tests/Bindings/Converters/ConverterMigrationHelperTests.cs index bf20d57a00..55ecbf0a37 100644 --- a/src/tests/ReactiveUI.Tests/Bindings/Converters/ConverterMigrationHelperTests.cs +++ b/src/tests/ReactiveUI.Tests/Bindings/Converters/ConverterMigrationHelperTests.cs @@ -18,8 +18,6 @@ public class ConverterMigrationHelperTests /// A representing the asynchronous unit test. [Test] public async Task ExtractConverters_ShouldThrowArgumentNullException_WhenResolverIsNull() => - - // Act & Assert await Assert.That(static () => ConverterMigrationHelperMixins.ExtractConverters(null!)) .Throws(); diff --git a/src/tests/ReactiveUI.Wpf.Tests/AssemblyHooks.cs b/src/tests/ReactiveUI.Wpf.Tests/AssemblyHooks.cs index 80efcb2f44..6c673d901e 100644 --- a/src/tests/ReactiveUI.Wpf.Tests/AssemblyHooks.cs +++ b/src/tests/ReactiveUI.Wpf.Tests/AssemblyHooks.cs @@ -15,11 +15,9 @@ namespace ReactiveUI.Tests; public static class AssemblyHooks { /// Called before any tests in this assembly start. + /// Overrides so the framework detects a unit test runner. [Before(Assembly)] - public static void AssemblySetup() => - - // Override ModeDetector to ensure we're detected as being in a unit test runner - ModeDetector.OverrideModeDetector(new TestModeDetector()); + public static void AssemblySetup() => ModeDetector.OverrideModeDetector(new TestModeDetector()); /// Mode detector that always indicates we're in a unit test runner. private sealed class TestModeDetector : IModeDetector