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
1,888 changes: 1,888 additions & 0 deletions dotnet-install.sh

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions src/ReactiveUI.Tests/AutoPersist/AutoPersistHelperTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ namespace ReactiveUI.Tests;
/// <summary>
/// Tests the AutoPersistHelper.
/// </summary>
/// <remarks>
/// This test fixture is marked as NonParallelizable because it uses HostTestFixture
/// which depends on ICreatesObservableForProperty from the service locator.
/// The service locator state must not be mutated concurrently by parallel tests.
/// </remarks>
[TestFixture]
[NonParallelizable]
public class AutoPersistHelperTest
{
/// <summary>
Expand Down
23 changes: 23 additions & 0 deletions src/ReactiveUI.Tests/AwaiterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,36 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using ReactiveUI.Tests.Infrastructure.StaticState;

namespace ReactiveUI.Tests;

/// <summary>
/// Tests the awaiters.
/// </summary>
/// <remarks>
/// This test fixture is marked as NonParallelizable because it accesses RxApp.TaskpoolScheduler,
/// which is global static state. While this test only reads the scheduler, marking it as
/// NonParallelizable ensures no interference with other tests that might modify scheduler state.
/// </remarks>
[TestFixture]
[NonParallelizable]
public class AwaiterTest
{
private RxAppSchedulersScope? _schedulersScope;

[SetUp]
public void SetUp()
{
_schedulersScope = new RxAppSchedulersScope();
}

[TearDown]
public void TearDown()
{
_schedulersScope?.Dispose();
}

/// <summary>
/// A smoke test for Awaiters.
/// </summary>
Expand Down
21 changes: 21 additions & 0 deletions src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,41 @@
using DynamicData;
using Microsoft.Reactive.Testing;
using ReactiveUI.Testing;
using ReactiveUI.Tests.Infrastructure.StaticState;

namespace ReactiveUI.Tests;

/// <summary>
/// Tests for the ReactiveCommand class.
/// </summary>
/// <remarks>
/// This test fixture is marked as NonParallelizable because it calls RxApp.EnsureInitialized()
/// in the constructor, which initializes global static state including the service locator
/// and schedulers. This state must not be concurrently initialized by parallel tests.
/// </remarks>
[TestFixture]
[NonParallelizable]
public class ReactiveCommandTest
{
private RxAppSchedulersScope? _schedulersScope;

public ReactiveCommandTest()
{
RxApp.EnsureInitialized();
}

[SetUp]
public void SetUp()
{
_schedulersScope = new RxAppSchedulersScope();
}

[TearDown]
public void TearDown()
{
_schedulersScope?.Dispose();
}

/// <summary>
/// A test that determines whether this instance [can execute changed is available via ICommand].
/// </summary>
Expand Down Expand Up @@ -1554,7 +1575,7 @@
/// </summary>
[Test]
public void ResultIsTickedThroughSpecifiedScheduler() =>
new TestScheduler().WithAsync(static async scheduler =>

Check warning on line 1578 in src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs

View workflow job for this annotation

GitHub Actions / build / build-unix (ubuntu-latest)

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1578 in src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs

View workflow job for this annotation

GitHub Actions / build / build-unix (ubuntu-latest)

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1578 in src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs

View workflow job for this annotation

GitHub Actions / build / build-unix (ubuntu-latest)

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1578 in src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs

View workflow job for this annotation

GitHub Actions / build / build-unix (ubuntu-latest)

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1578 in src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs

View workflow job for this annotation

GitHub Actions / build / build-unix (macos-latest)

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1578 in src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs

View workflow job for this annotation

GitHub Actions / build / build-unix (macos-latest)

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1578 in src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs

View workflow job for this annotation

GitHub Actions / build / build-unix (macos-latest)

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1578 in src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs

View workflow job for this annotation

GitHub Actions / build / build-unix (macos-latest)

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1578 in src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs

View workflow job for this annotation

GitHub Actions / build / build-windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1578 in src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs

View workflow job for this annotation

GitHub Actions / build / build-windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1578 in src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs

View workflow job for this annotation

GitHub Actions / build / build-windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1578 in src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs

View workflow job for this annotation

GitHub Actions / build / build-windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1578 in src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs

View workflow job for this annotation

GitHub Actions / build / build-windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1578 in src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs

View workflow job for this annotation

GitHub Actions / build / build-windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1578 in src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs

View workflow job for this annotation

GitHub Actions / build / build-windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1578 in src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs

View workflow job for this annotation

GitHub Actions / build / build-windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
var fixture = ReactiveCommand.CreateRunInBackground(
static () => Observables.Unit,
Expand Down
69 changes: 69 additions & 0 deletions src/ReactiveUI.Tests/Infrastructure/StaticState/MessageBusScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

namespace ReactiveUI.Tests.Infrastructure.StaticState;

/// <summary>
/// A disposable scope that snapshots and restores MessageBus.Current static state.
/// Use this in test fixtures that read or modify MessageBus.Current to ensure
/// static state is properly restored after tests complete.
/// </summary>
/// <remarks>
/// This helper is necessary because MessageBus.Current maintains a static/global reference
/// that can leak between parallel test executions, causing intermittent failures.
/// Tests using this scope should also be marked with [NonParallelizable] to prevent
/// concurrent modifications to the shared state.
/// </remarks>
/// <example>
/// <code>
/// [TestFixture]
/// [NonParallelizable]
/// public class MyTests
/// {
/// private MessageBusScope _messageBusScope;
///
/// [SetUp]
/// public void SetUp()
/// {
/// _messageBusScope = new MessageBusScope();
/// // Now safe to use MessageBus.Current
/// }
///
/// [TearDown]
/// public void TearDown()
/// {
/// _messageBusScope?.Dispose();
/// }
/// }
/// </code>
/// </example>
public sealed class MessageBusScope : IDisposable
{
private readonly IMessageBus _previousMessageBus;
private bool _disposed;

/// <summary>
/// Initializes a new instance of the <see cref="MessageBusScope"/> class.
/// Snapshots the current MessageBus.Current state.
/// </summary>
public MessageBusScope()
{
_previousMessageBus = MessageBus.Current;
}

/// <summary>
/// Restores the MessageBus.Current state to what it was when this scope was created.
/// </summary>
public void Dispose()
{
if (_disposed)
{
return;
}

MessageBus.Current = _previousMessageBus;
_disposed = true;
}
}
144 changes: 144 additions & 0 deletions src/ReactiveUI.Tests/Infrastructure/StaticState/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Static State Test Isolation

This directory contains helper classes for managing static/global state in ReactiveUI tests.

## Problem

ReactiveUI uses several static/global entry points for configuration:
- `RxApp.MainThreadScheduler` and `RxApp.TaskpoolScheduler`
- `RxApp.EnsureInitialized()` (initializes the service locator)
- `MessageBus.Current`
- `Locator.Current` / `Locator.CurrentMutable` (from Splat)

When tests access or modify these static states, they can cause interference between test executions, leading to intermittent failures and hidden state pollution.

## Solution: NonParallelizable + State Restoration

Tests that use static state should be marked `[NonParallelizable]` **AND** use state restoration scopes to ensure clean state between tests.

### Available Helper Scopes

#### 1. RxAppSchedulersScope

Snapshots and restores `RxApp.MainThreadScheduler` and `RxApp.TaskpoolScheduler`.

**Usage:**
```csharp
[TestFixture]
[NonParallelizable]
public class MyTests
{
private RxAppSchedulersScope? _schedulersScope;

[SetUp]
public void SetUp()
{
_schedulersScope = new RxAppSchedulersScope();
// Now safe to modify RxApp schedulers
}

[TearDown]
public void TearDown()
{
_schedulersScope?.Dispose();
}
}
```

#### 2. MessageBusScope

Snapshots and restores `MessageBus.Current`.

**Usage:**
```csharp
[TestFixture]
[NonParallelizable]
public class MyTests
{
private MessageBusScope? _messageBusScope;

[SetUp]
public void SetUp()
{
_messageBusScope = new MessageBusScope();
// Now safe to use or replace MessageBus.Current
}

[TearDown]
public void TearDown()
{
_messageBusScope?.Dispose();
}
}
```

#### 3. StaticStateScope (Generic)

A generic helper for capturing and restoring arbitrary static state using getter/setter pairs.

**Usage:**
```csharp
[TestFixture]
[NonParallelizable]
public class MyTests
{
private StaticStateScope? _stateScope;

[SetUp]
public void SetUp()
{
_stateScope = new StaticStateScope(
() => MyClass.StaticProperty,
(object? value) => MyClass.StaticProperty = value,
() => AnotherClass.StaticField,
(object? value) => AnotherClass.StaticField = value);
}

[TearDown]
public void TearDown()
{
_stateScope?.Dispose();
}
}
```

## When to Use These Scopes

**Always use state restoration scopes when:**

1. The test calls `RxApp.EnsureInitialized()`
2. The test modifies `RxApp` properties (schedulers, SuspensionHost, etc.)
3. The test modifies `MessageBus.Current`
4. The test accesses or modifies `Locator.CurrentMutable`
5. The test creates instances that depend on service locator registrations

**Why both NonParallelizable AND state restoration?**

- `[NonParallelizable]` prevents concurrent access issues
- State restoration ensures clean state even when tests run sequentially
- This prevents hidden state pollution that can cause issues in future test runs

## Important Notes

1. **Always mark test fixtures as `[NonParallelizable]`** when using these scopes
2. **State restoration does NOT make tests safe for parallel execution** - it only ensures cleanup
3. **For Splat's Locator**: Due to API complexity, use `StaticStateScope` or manual cleanup if needed

## Test Fixtures Already Marked as NonParallelizable

The following test fixtures are marked `[NonParallelizable]` because they use static/global state:

- `AutoPersistHelperTest` - uses HostTestFixture which depends on service locator
- `MessageBusTest` - uses Locator.CurrentMutable and MessageBus.Current
- `RandomTests` - uses RxApp, Locator.CurrentMutable, MessageBus.Current
- `RxAppTest` - accesses RxApp.MainThreadScheduler
- `ReactiveCommandTest` - calls RxApp.EnsureInitialized() in constructor
- `PocoObservableForPropertyTests` - calls RxApp.EnsureInitialized()
- `AwaiterTest` - accesses RxApp.TaskpoolScheduler
- `RxAppDependencyObjectTests` - calls RxApp.EnsureInitialized() and accesses Locator.Current
- `RoutedViewHostTests` - uses Locator.CurrentMutable to register services
- `ViewModelViewHostTests` - uses Locator.CurrentMutable.Register()
- `WpfCommandBindingImplementationTests` - registers test logger in Locator
- `DefaultPropertyBindingTests` - calls RxApp.EnsureInitialized() in constructor

Each of these fixtures has XML documentation explaining the specific reason for being NonParallelizable.
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

namespace ReactiveUI.Tests.Infrastructure.StaticState;

/// <summary>
/// A disposable scope that snapshots and restores RxApp scheduler state.
/// Use this in test fixtures that modify RxApp.MainThreadScheduler or RxApp.TaskpoolScheduler
/// to ensure static state is properly restored after tests complete.
/// </summary>
/// <remarks>
/// This helper is necessary because RxApp maintains static/global scheduler references
/// that can leak between parallel test executions, causing intermittent failures.
/// Tests using this scope should also be marked with [NonParallelizable] to prevent
/// concurrent modifications to the shared state.
/// </remarks>
/// <example>
/// <code>
/// [TestFixture]
/// [NonParallelizable]
/// public class MyTests
/// {
/// private RxAppSchedulersScope _schedulersScope;
///
/// [SetUp]
/// public void SetUp()
/// {
/// _schedulersScope = new RxAppSchedulersScope();
/// // Now safe to modify RxApp schedulers
/// }
///
/// [TearDown]
/// public void TearDown()
/// {
/// _schedulersScope?.Dispose();
/// }
/// }
/// </code>
/// </example>
public sealed class RxAppSchedulersScope : IDisposable
{
private readonly IScheduler _mainThreadScheduler;
private readonly IScheduler _taskpoolScheduler;
private bool _disposed;

/// <summary>
/// Initializes a new instance of the <see cref="RxAppSchedulersScope"/> class.
/// Snapshots the current RxApp scheduler state.
/// </summary>
public RxAppSchedulersScope()
{
_mainThreadScheduler = RxApp.MainThreadScheduler;
_taskpoolScheduler = RxApp.TaskpoolScheduler;
}

/// <summary>
/// Restores the RxApp scheduler state to what it was when this scope was created.
/// </summary>
public void Dispose()
{
if (_disposed)
{
return;
}

RxApp.MainThreadScheduler = _mainThreadScheduler;
RxApp.TaskpoolScheduler = _taskpoolScheduler;
_disposed = true;
}
}
Loading
Loading