Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 0 additions & 49 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
<PropertyGroup Label="Shared Version Variables">
<SplatVersion>20.2.0</SplatVersion>
<PrimitivesVersion>7.1.0</PrimitivesVersion>
<TUnitVersion>1.61.38</TUnitVersion>
<TUnitVersion>1.63.0</TUnitVersion>
<!-- StyleSharp.Analyzers, PerformanceSharp.Analyzers and SecuritySharp.Analyzers ship from the
same release pipeline and always share a version. -->
<RoslynCommonAnalyzersVersion>3.38.1</RoslynCommonAnalyzersVersion>
<RoslynCommonAnalyzersVersion>3.40.1</RoslynCommonAnalyzersVersion>
<XamarinAndroidXLifecycleLiveDataVersion>2.11.0.1</XamarinAndroidXLifecycleLiveDataVersion>
</PropertyGroup>

Expand Down Expand Up @@ -77,7 +77,7 @@
<PackageVersion Include="TUnit.Core" Version="$(TUnitVersion)"/>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1"/>
<PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage" Version="18.9.0"/>
<PackageVersion Include="Microsoft.Testing.Platform.MSBuild" Version="2.3.2"/>
<PackageVersion Include="Microsoft.Testing.Platform.MSBuild" Version="2.3.3"/>
<PackageVersion Include="Microsoft.Reactive.Testing" Version="7.0.0"/>
<PackageVersion Include="System.Reactive" Version="7.0.0"/>
<PackageVersion Include="Mocks.Maui" Version="1.2.5"/>
Expand All @@ -88,7 +88,7 @@
<PackageVersion Include="PerformanceSharp.Analyzers" Version="$(RoslynCommonAnalyzersVersion)"/>
<PackageVersion Include="SecuritySharp.Analyzers" Version="$(RoslynCommonAnalyzersVersion)"/>
<PackageVersion Include="Roslynator.Analyzers" Version="4.15.0"/>
<PackageVersion Include="SonarAnalyzer.CSharp" Version="10.30.0.144632"/>
<PackageVersion Include="SonarAnalyzer.CSharp" Version="10.31.0.145097"/>
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.6.0"/>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.6.0"/>
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="5.6.0"/>
Expand Down
10 changes: 5 additions & 5 deletions src/tests/ReactiveUI.AOTTests/AssemblyHooks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ namespace ReactiveUI.AOT.Tests;
public static class AssemblyHooks
{
/// <summary>Called before any tests in this assembly start.</summary>
/// <remarks>
/// Overrides <see cref="ModeDetector"/> so the framework detects a unit test runner.
/// App builder initialization is handled per-test via AppBuilderTestExecutor.
/// </remarks>
[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());

/// <summary>Mode detector that always indicates we're in a unit test runner.</summary>
private sealed class TestModeDetector : IModeDetector
Expand Down
10 changes: 5 additions & 5 deletions src/tests/ReactiveUI.Builder.Tests/AssemblyHooks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ namespace ReactiveUI.Builder.Tests;
public static class AssemblyHooks
{
/// <summary>Called before any tests in this assembly start.</summary>
/// <remarks>
/// Overrides <see cref="ModeDetector"/> so the framework detects a unit test runner.
/// App builder initialization is handled per-test via AppBuilderTestExecutor.
/// </remarks>
[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());

/// <summary>Mode detector that always indicates we're in a unit test runner.</summary>
private sealed class TestModeDetector : IModeDetector
Expand Down
6 changes: 2 additions & 4 deletions src/tests/ReactiveUI.Maui.Tests/AssemblyHooks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,9 @@ namespace ReactiveUI.Maui.Tests;
public static class AssemblyHooks
{
/// <summary>Called before any tests in this assembly start.</summary>
/// <remarks>Overrides <see cref="ModeDetector"/> so the framework detects a unit test runner.</remarks>
[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());

/// <summary>Mode detector that always indicates we're in a unit test runner.</summary>
private sealed class TestModeDetector : IModeDetector
Expand Down
14 changes: 7 additions & 7 deletions src/tests/ReactiveUI.Routing.Tests/AssemblyHooks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ namespace ReactiveUI.Tests;
public static class AssemblyHooks
{
/// <summary>Called before any tests in this assembly start.</summary>
/// <remarks>
/// 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.
/// </remarks>
[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());

/// <summary>Mode detector that always indicates we're in a unit test runner.</summary>
private sealed class TestModeDetector : IModeDetector
Expand Down
6 changes: 2 additions & 4 deletions src/tests/ReactiveUI.Splat.Tests/AssemblyHooks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,9 @@ namespace ReactiveUI.Splat.Tests;
public static class AssemblyHooks
{
/// <summary>Called before any tests in this assembly start.</summary>
/// <remarks>Overrides <see cref="ModeDetector"/> so the framework detects a unit test runner.</remarks>
[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());

/// <summary>Mode detector that always indicates we're in a unit test runner.</summary>
private sealed class TestModeDetector : IModeDetector
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,16 @@ public class RaceConditionFixture : ReactiveObject
private readonly ObservableAsPropertyHelper<bool> _a;

/// <summary>Initializes a new instance of the <see cref="RaceConditionFixture"/> class.</summary>
/// <remarks>
/// Emits a value on subscription that differs from the default, which triggers the property
/// change firing during the <see cref="ObservableAsPropertyHelper{T}"/> constructor - the race
/// this fixture exists to reproduce.
/// </remarks>
[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);

/// <summary>Gets or sets the count.</summary>
public int Count { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,16 @@ public class RaceConditionNameOfFixture : ReactiveObject
private readonly ObservableAsPropertyHelper<bool> _a;

/// <summary>Initializes a new instance of the <see cref="RaceConditionNameOfFixture"/> class.</summary>
/// <remarks>
/// Emits a value on subscription that differs from the default, which triggers the property
/// change firing during the <see cref="ObservableAsPropertyHelper{T}"/> constructor - the race
/// this fixture exists to reproduce.
/// </remarks>
[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);
Expand Down
4 changes: 0 additions & 4 deletions src/tests/ReactiveUI.Testing.Tests/AppBuilderTestBaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ public async Task RunAppBuilderTestAsync_WithSyncTestBody_ExecutesTest()
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Test]
public async Task RunAppBuilderTestAsync_WithAsyncTestBody_PropagatesExceptions() =>

// Act & Assert
await Assert.That(static async () =>
{
await TestHelper.RunAppBuilderTestAsync(static async () =>
Expand All @@ -66,8 +64,6 @@ await TestHelper.RunAppBuilderTestAsync(static async () =>
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Test]
public async Task RunAppBuilderTestAsync_WithSyncTestBody_PropagatesExceptions() =>

// Act & Assert
await Assert.That(static async () => await TestHelper.RunAppBuilderTestAsync(static () => throw new InvalidOperationException("Test exception"))).Throws<InvalidOperationException>();

/// <summary>Verifies that <see cref="AppBuilderTestBase.RunAppBuilderTestAsync(Action)"/> can be called multiple times sequentially without interference.</summary>
Expand Down
10 changes: 5 additions & 5 deletions src/tests/ReactiveUI.Testing.Tests/AssemblyHooks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ namespace ReactiveUI.Testing.Tests;
public static class AssemblyHooks
{
/// <summary>Called before any tests in this assembly start.</summary>
/// <remarks>
/// Overrides <see cref="ModeDetector"/> so the framework detects a unit test runner.
/// App builder initialization is handled per-test via test executors.
/// </remarks>
[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());

/// <summary>Mode detector that always indicates we're in a unit test runner.</summary>
private sealed class TestModeDetector : IModeDetector
Expand Down
7 changes: 0 additions & 7 deletions src/tests/ReactiveUI.Testing.Tests/RxTestTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ await Assert.That(static () => RxTest.AppBuilderTestAsync(null!))
/// <exception cref="InvalidOperationException">Thrown if the delegate passed to AppBuilderTestAsync throws an InvalidOperationException.</exception>
[Test]
public async Task AppBuilderTestAsync_PropagatesExceptions() =>

// Act & Assert
await Assert.That(static async () => await RxTest.AppBuilderTestAsync(static () => throw new InvalidOperationException("Test exception"))).Throws<InvalidOperationException>();

/// <summary>
Expand Down Expand Up @@ -161,8 +159,6 @@ await RxTest.AppBuilderTestAsync(
/// <returns>A task that represents the asynchronous test operation.</returns>
[Test]
public async Task AppBuilderTestAsync_ThrowsTimeoutException_WhenTestExceedsTimeout() =>

// Act & Assert
await Assert.That(static async () =>
{
await RxTest.AppBuilderTestAsync(
Expand All @@ -180,8 +176,5 @@ static async () =>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Test]
public Task AppBuilderTestAsync_HandlesTaskCompletedTask() =>

// This test verifies that returning Task.CompletedTask works correctly
// Act - Should not throw
RxTest.AppBuilderTestAsync(static () => Task.CompletedTask);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ public class ConverterMigrationHelperTests
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Test]
public async Task ExtractConverters_ShouldThrowArgumentNullException_WhenResolverIsNull() =>

// Act & Assert
await Assert.That(static () => ConverterMigrationHelperMixins.ExtractConverters(null!))
.Throws<ArgumentException>();

Expand Down
6 changes: 2 additions & 4 deletions src/tests/ReactiveUI.Wpf.Tests/AssemblyHooks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,9 @@ namespace ReactiveUI.Tests;
public static class AssemblyHooks
{
/// <summary>Called before any tests in this assembly start.</summary>
/// <remarks>Overrides <see cref="ModeDetector"/> so the framework detects a unit test runner.</remarks>
[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());

/// <summary>Mode detector that always indicates we're in a unit test runner.</summary>
private sealed class TestModeDetector : IModeDetector
Expand Down
Loading