From 33e0f6967c9169c1cf3a2ded4402291394233132 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Sat, 12 Sep 2026 00:44:47 +0000
Subject: [PATCH 1/6] chore(deps): update dependency tunit to 1.66.27
---
src/Directory.Packages.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index d1615cfe..d97feb54 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -68,7 +68,7 @@
-
+
From 9e370f8a7ba0ffd191d230f516cbcbe6b65ff069 Mon Sep 17 00:00:00 2001
From: Chris Pulman
Date: Sat, 12 Sep 2026 05:06:05 +0100
Subject: [PATCH 2/6] test(concurrency): preserve Wasm due-time checks under CI
contention
- Allow instrumented runners 30 seconds to complete scheduled callbacks.
- Verify delayed work executes at or after its requested due time.
- Retain all execution, cancellation and timing assertions.
Validation: all Primitives TUnit tests passed on net8-net11 in the reviewed feature CI repair; matching Wasm class coverage is 100% lines and branches on all four targets. Feature PR197 Windows, Linux, macOS and coverage jobs are now green with this same patch.
---
.../ReactiveUI.Primitives.Tests/WasmSequencerTests.cs | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/src/tests/ReactiveUI.Primitives.Tests/WasmSequencerTests.cs b/src/tests/ReactiveUI.Primitives.Tests/WasmSequencerTests.cs
index bd73cb81..1148d50f 100644
--- a/src/tests/ReactiveUI.Primitives.Tests/WasmSequencerTests.cs
+++ b/src/tests/ReactiveUI.Primitives.Tests/WasmSequencerTests.cs
@@ -14,8 +14,8 @@ public sealed class WasmSequencerTests
/// Expected values produced by an immediate burst, used to verify FIFO order.
private static readonly int[] ExpectedBurst = [1, 2, 3];
- /// Longest a test waits for scheduled work before failing.
- private static readonly TimeSpan WaitTimeout = TimeSpan.FromSeconds(5);
+ /// Completion guard allowing for shared timer and thread-pool contention on instrumented runners.
+ private static readonly TimeSpan WaitTimeout = TimeSpan.FromSeconds(30);
/// How far in the future delayed work is scheduled.
private static readonly TimeSpan ScheduleDelay = TimeSpan.FromMilliseconds(50);
@@ -104,13 +104,12 @@ public async Task DelayedScheduleExecutesAfterDue()
{
var sequencer = WasmSequencer.Default;
TaskCompletionSource executed = new(TaskCreationOptions.RunContinuationsAsynchronously);
- var start = sequencer.Timestamp;
- var due = Sequencer.AddTimestamp(start, ScheduleDelay);
+ var due = Sequencer.AddTimestamp(sequencer.Timestamp, ScheduleDelay);
sequencer.Schedule(new DelegateWorkItem(() => executed.TrySetResult(sequencer.Timestamp)), due);
var executedAt = await executed.Task.WaitAsync(WaitTimeout);
- await Assert.That(executedAt).IsGreaterThanOrEqualTo(start);
+ await Assert.That(executedAt).IsGreaterThanOrEqualTo(due);
}
/// Verifies a past-due timestamp executes promptly through the immediate path.
From 55afb31c9d2088464d763f06846e29c7dc3f2fec Mon Sep 17 00:00:00 2001
From: Chris Pulman
Date: Sat, 12 Sep 2026 05:06:06 +0100
Subject: [PATCH 3/6] test(signals): allow instrumented async enumeration to
complete
- Use a named 30-second completion guard for the scheduled async enumeration test.
- Dispose its subscription after verification.
- Preserve ordered values and successful completion assertions.
Validation: focused TUnit test passed on net8-net11 and the full Primitives suites passed on all four targets in the reviewed feature CI repair. No test skips or suppressions added.
---
.../SignalOperatorMixinsTests.Deterministic.cs | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/tests/ReactiveUI.Primitives.Tests/SignalOperatorMixinsTests.Deterministic.cs b/src/tests/ReactiveUI.Primitives.Tests/SignalOperatorMixinsTests.Deterministic.cs
index c523fbfd..d50f8f8b 100644
--- a/src/tests/ReactiveUI.Primitives.Tests/SignalOperatorMixinsTests.Deterministic.cs
+++ b/src/tests/ReactiveUI.Primitives.Tests/SignalOperatorMixinsTests.Deterministic.cs
@@ -48,6 +48,9 @@ public partial class SignalOperatorMixinsTests
/// The number of threads that rendezvous before the disposal race starts.
private const int RacingThreadCount = 2;
+ /// The completion guard for asynchronously scheduled enumeration on instrumented CI hosts.
+ private static readonly TimeSpan AsyncEnumerationCompletionTimeout = TimeSpan.FromSeconds(30);
+
/// A fixed deterministic timestamp used in place of the current time.
private static readonly DateTimeOffset FixedTimestamp = new(2024, 1, 1, 0, 0, 0, TimeSpan.Zero);
@@ -469,11 +472,11 @@ private static async Task VerifyAsyncEnumerableShiftAndExpireAsync()
List asyncValues = [];
TaskCompletionSource asyncCompleted = new(TaskCreationOptions.RunContinuationsAsynchronously);
using CancellationTokenSource asyncToken = new();
- _ = Signal.FromAsyncEnumerable(AsyncValues(Three), asyncToken.Token).Subscribe(
+ using var asyncSubscription = Signal.FromAsyncEnumerable(AsyncValues(Three), asyncToken.Token).Subscribe(
asyncValues.Add,
ex => asyncCompleted.TrySetException(ex),
() => asyncCompleted.TrySetResult(null));
- await asyncCompleted.Task.WaitAsync(TimeSpan.FromSeconds(Five)).ConfigureAwait(false);
+ await asyncCompleted.Task.WaitAsync(AsyncEnumerationCompletionTimeout).ConfigureAwait(false);
int[] expectedAsyncValues = [0, One, Two];
await Assert.That(asyncValues.SequenceEqual(expectedAsyncValues)).IsTrue();
var exact = await Signal.FromAsyncEnumerable(AsyncValues(Sixteen)).CollectArrayAsync().ConfigureAwait(false);
From bc1d7ed930f72693dfaaef6a704cf6d0d4fee952 Mon Sep 17 00:00:00 2001
From: Chris Pulman
Date: Sat, 12 Sep 2026 05:13:55 +0100
Subject: [PATCH 4/6] test(concurrency): coordinate timeout supersession and
rearming
- Drive queued timeout attempts with an explicit monotonic clock instead of a settle delay.
- Join both started workers before releasing owned observer resources.
- Allow instrumented runners a 30-second guard while preserving overlap checks.
- Assert the obsolete timer produces no error and the replacement produces exactly one timeout.
- Remove the observer resource-ownership suppression through proper disposal.
Validation: root reviewed scheduler semantics and corrected stopwatch clock conversion; full Primitives TUnit suites pass across net8-net11 (909/911/911/911), with zero skipped tests and clean Release builds.
---
.../ExpireCoordinatorTests.cs | 154 ++++++++++++++----
1 file changed, 122 insertions(+), 32 deletions(-)
diff --git a/src/tests/ReactiveUI.Primitives.Tests/ExpireCoordinatorTests.cs b/src/tests/ReactiveUI.Primitives.Tests/ExpireCoordinatorTests.cs
index f0a4d714..5585d565 100644
--- a/src/tests/ReactiveUI.Primitives.Tests/ExpireCoordinatorTests.cs
+++ b/src/tests/ReactiveUI.Primitives.Tests/ExpireCoordinatorTests.cs
@@ -2,6 +2,8 @@
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.
+using System.Collections.Concurrent;
+using System.Runtime.CompilerServices;
using ReactiveUI.Primitives.Advanced;
using ReactiveUI.Primitives.Concurrency;
using ReactiveUI.Primitives.Signals;
@@ -30,10 +32,7 @@ public sealed class ExpireCoordinatorTests
private static readonly int[] ExpectedActiveValues = [0, 1, 2, 3, 4];
/// Timeout used while waiting for background work in this test.
- private static readonly TimeSpan WaitTimeout = TimeSpan.FromSeconds(5);
-
- /// How long the superseded timeout is given to reach the observer before the invariant is checked.
- private static readonly TimeSpan RaceSettleDelay = TimeSpan.FromMilliseconds(50);
+ private static readonly TimeSpan WaitTimeout = TimeSpan.FromSeconds(30);
/// Verifies the timeout re-arms on each value so an active source never expires.
/// A task representing the asynchronous operation.
@@ -174,29 +173,46 @@ public async Task ValueArrivingInsideTheWindowIsForwardedWhileTheTimeoutIsStillU
[Test]
public async Task TimeoutDoesNotEnterObserverWhileOnNextIsInFlight()
{
- VirtualClock clock = new(DateTimeOffset.UnixEpoch);
+ QueuedSequencer sequencer = new();
Signal source = new();
- BlockingObserver observer = new();
- using var subscription = source.Expire(TimeSpan.FromTicks(One), clock).Subscribe(observer);
+ using var observer = new BlockingObserver();
+ using var subscription = source.Expire(TimeSpan.FromTicks(One), sequencer).Subscribe(observer);
- // Dedicated threads rather than the pool: the observer parks its caller inside OnNext until this
- // test releases it, so on the pool that notification holds a worker while the timeout waits behind
- // it in the queue. A saturated pool then starves the very interleaving under test.
var onNextFinished = RunOnDedicatedThread(() => source.OnNext(One));
- await observer.OnNextEntered.Task.WaitAsync(WaitTimeout).ConfigureAwait(false);
+ Task? timeoutFinished = null;
+ try
+ {
+ await observer.OnNextEntered.Task.WaitAsync(WaitTimeout).ConfigureAwait(false);
- var timeoutFinished = RunOnDedicatedThread(() => clock.AdvanceBy(TimeSpan.FromTicks(One)));
- await Task.Delay(RaceSettleDelay).ConfigureAwait(false);
+ // Queue the initial timeout for a dedicated worker while OnNext owns the coordinator gate. The assertion
+ // below is only about observer serialization: it must not report an error while the value callback is
+ // still active. Releasing OnNext lets the source re-arm its replacement timer and dispose this timer.
+ timeoutFinished = RunOnDedicatedThread(sequencer.ExecuteNext);
+ await sequencer.TimeoutExecutionStarted.Task.WaitAsync(WaitTimeout).ConfigureAwait(false);
- await Assert.That(observer.ErrorEnteredDuringOnNext).IsFalse();
-
- observer.ReleaseOnNext.Set();
- await onNextFinished.WaitAsync(WaitTimeout).ConfigureAwait(false);
- await timeoutFinished.WaitAsync(WaitTimeout).ConfigureAwait(false);
+ await Assert.That(observer.ErrorEnteredDuringOnNext).IsFalse();
+ }
+ finally
+ {
+ observer.ReleaseOnNext.Set();
+ if (timeoutFinished is not null)
+ {
+ await Task.WhenAll(onNextFinished, timeoutFinished).WaitAsync(WaitTimeout).ConfigureAwait(false);
+ }
+ else
+ {
+ await onNextFinished.WaitAsync(WaitTimeout).ConfigureAwait(false);
+ }
+ }
- // Timeout may be observed after OnNext exits depending on scheduler timing.
- // The invariant required here is that OnError never re-enters while OnNext is active.
- await Assert.That(observer.Errors).IsLessThanOrEqualTo(One);
+ // The initial timer attempt must not terminate the sequence after the value wins the race. Executing the
+ // replacement proves that the successful value re-armed the inactivity timeout.
+ await Assert.That(observer.ErrorEnteredDuringOnNext).IsFalse();
+ await Assert.That(observer.Errors).IsEqualTo(0);
+ await Assert.That(observer.Values).IsEqualTo(One);
+ sequencer.ExecuteNext();
+ await Assert.That(observer.Errors).IsEqualTo(One);
+ await Assert.That(observer.TimeoutErrors).IsEqualTo(One);
await Assert.That(observer.Values).IsEqualTo(One);
}
@@ -265,14 +281,7 @@ private sealed class UndispatchedSequencer(DateTimeOffset start) : ISequencer
}
/// Observer that blocks source value handling so timeout serialization can be observed.
- [System.Diagnostics.CodeAnalysis.SuppressMessage(
- "Design",
- "SST2315:A type that owns a disposable should be disposable",
- Justification =
- "Test double that owns a ManualResetEventSlim used to gate OnNext so the test can observe timeout "
- + "serialization. Its lifetime is the test's; the test process owns and releases it, so it is deliberately "
- + "not IDisposable.")]
- private sealed class BlockingObserver : IObserver
+ private sealed class BlockingObserver : IObserver, IDisposable
{
/// Non-zero while is active. Written by the notifying thread and read by
/// the timeout thread, so the two must not race on a plain field.
@@ -288,6 +297,9 @@ private sealed class BlockingObserver : IObserver
/// The number of forwarded errors.
private int _errors;
+ /// The number of forwarded timeout errors.
+ private int _timeoutErrors;
+
/// Gets the task completed when is entered.
public TaskCompletionSource OnNextEntered { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -300,9 +312,16 @@ private sealed class BlockingObserver : IObserver
/// Gets the number of forwarded errors.
public int Errors => Volatile.Read(ref _errors);
+ /// Gets the number of forwarded timeout errors.
+ public int TimeoutErrors => Volatile.Read(ref _timeoutErrors);
+
/// Gets a value indicating whether an error entered while was active.
public bool ErrorEnteredDuringOnNext => Volatile.Read(ref _errorEnteredDuringOnNext) != 0;
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Dispose() => ReleaseOnNext.Dispose();
+
///
public void OnCompleted()
{
@@ -316,6 +335,11 @@ public void OnError(Exception error)
Volatile.Write(ref _errorEnteredDuringOnNext, 1);
}
+ if (error is TimeoutException)
+ {
+ _ = Interlocked.Increment(ref _timeoutErrors);
+ }
+
_ = Interlocked.Increment(ref _errors);
}
@@ -324,9 +348,75 @@ public void OnNext(int value)
{
_ = Interlocked.Increment(ref _values);
Volatile.Write(ref _isInOnNext, 1);
- OnNextEntered.SetResult();
- _ = ReleaseOnNext.Wait(WaitTimeout);
- Volatile.Write(ref _isInOnNext, 0);
+ _ = OnNextEntered.TrySetResult();
+ try
+ {
+ if (!ReleaseOnNext.Wait(WaitTimeout))
+ {
+ throw new TimeoutException("The test did not release the in-flight OnNext callback.");
+ }
+ }
+ finally
+ {
+ Volatile.Write(ref _isInOnNext, 0);
+ }
+ }
+ }
+
+ /// Sequencer that queues work until this test explicitly executes it.
+ private sealed class QueuedSequencer : ISequencer
+ {
+ /// Scheduled work waiting for the test to execute it.
+ private readonly ConcurrentQueue<(IWorkItem Item, long DueTimestamp)> _items = new();
+
+ /// The current monotonic timestamp, updated before a queued item is invoked.
+ private long _timestamp;
+
+ /// Gets the task completed when a worker dequeues a queued timeout for execution.
+ public TaskCompletionSource TimeoutExecutionStarted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ ///
+ public DateTimeOffset Now => DateTimeOffset.UnixEpoch + Sequencer.ToTimeSpanDelta(Timestamp);
+
+ ///
+ public long Timestamp => Volatile.Read(ref _timestamp);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Schedule(IWorkItem item) => _items.Enqueue((item, Timestamp));
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Schedule(IWorkItem item, long dueTimestamp) => _items.Enqueue((item, dueTimestamp));
+
+ /// Executes the next queued work item.
+ /// No timer was queued when execution was requested.
+ public void ExecuteNext()
+ {
+ if (!_items.TryDequeue(out var scheduled))
+ {
+ throw new InvalidOperationException("No queued timeout was available to execute.");
+ }
+
+ AdvanceTo(scheduled.DueTimestamp);
+ _ = TimeoutExecutionStarted.TrySetResult();
+ scheduled.Item.Execute();
+ }
+
+ /// Advances the clock to a scheduled due timestamp without moving it backwards.
+ /// The timestamp of the work about to execute.
+ private void AdvanceTo(long dueTimestamp)
+ {
+ long currentTimestamp;
+ do
+ {
+ currentTimestamp = Timestamp;
+ if (currentTimestamp >= dueTimestamp)
+ {
+ return;
+ }
+ }
+ while (Interlocked.CompareExchange(ref _timestamp, dueTimestamp, currentTimestamp) != currentTimestamp);
}
}
}
From 21e2bb6906cb66717345acffe91868c19cafe937 Mon Sep 17 00:00:00 2001
From: Chris Pulman
Date: Sat, 12 Sep 2026 05:13:56 +0100
Subject: [PATCH 5/6] test(concurrency): control delayed dispatcher
cancellation ordering
- Compose SynchronizationContextSequencer with an internal delayed scheduler dependency.
- Keep the public constructor on the shared thread-pool scheduler.
- Cancel queued work before explicitly dispatching the delayed callback in the regression test.
- Cover non-cancelled dispatch and missing dependency validation; retain the real timer integration test.
Validation: inverted cancellation guard compiles and fails the posting assertion before restoration. Full Primitives TUnit suites pass on net8-net11 (909/911/911/911). MTP reports 100% measured lines and branches for SynchronizationContextSequencer on every target. Library builds all target frameworks without warnings or errors; no new suppression or public API change.
---
.../SynchronizationContextSequencer.cs | 19 ++++-
.../SequencerTests.Pools.cs | 77 +++++++++++++++++--
2 files changed, 89 insertions(+), 7 deletions(-)
diff --git a/src/ReactiveUI.Primitives/Concurrency/SynchronizationContextSequencer.cs b/src/ReactiveUI.Primitives/Concurrency/SynchronizationContextSequencer.cs
index 12a06f08..2adb4901 100644
--- a/src/ReactiveUI.Primitives/Concurrency/SynchronizationContextSequencer.cs
+++ b/src/ReactiveUI.Primitives/Concurrency/SynchronizationContextSequencer.cs
@@ -9,11 +9,26 @@ namespace ReactiveUI.Primitives.Concurrency;
[System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")]
public sealed class SynchronizationContextSequencer : ISequencer
{
+ /// Schedules delayed dispatch without owning the scheduler lifetime.
+ private readonly ISequencer _delaySequencer;
+
/// Initializes a new instance of the class.
/// The synchronization context used to schedule work.
/// is .
- public SynchronizationContextSequencer(SynchronizationContext context) =>
+ public SynchronizationContextSequencer(SynchronizationContext context)
+ : this(context, ThreadPoolSequencer.Instance)
+ {
+ }
+
+ /// Initializes a new instance of the class.
+ /// The synchronization context used to schedule work.
+ /// The scheduler used to wait before posting delayed work.
+ /// Either dependency is .
+ internal SynchronizationContextSequencer(SynchronizationContext context, ISequencer delaySequencer)
+ {
Context = context ?? throw new ArgumentNullException(nameof(context));
+ _delaySequencer = delaySequencer ?? throw new ArgumentNullException(nameof(delaySequencer));
+ }
/// Gets a sequencer for the current synchronization context.
/// There is no current synchronization context.
@@ -58,7 +73,7 @@ public void Schedule(IWorkItem item, long dueTimestamp)
return;
}
- ThreadPoolSequencer.Instance.Schedule(new DelayedPostWorkItem(this, item), dueTimestamp);
+ _delaySequencer.Schedule(new DelayedPostWorkItem(this, item), dueTimestamp);
}
/// Executes work when it has not already been cancelled.
diff --git a/src/tests/ReactiveUI.Primitives.Tests/SequencerTests.Pools.cs b/src/tests/ReactiveUI.Primitives.Tests/SequencerTests.Pools.cs
index e1676ff6..0b74ebdf 100644
--- a/src/tests/ReactiveUI.Primitives.Tests/SequencerTests.Pools.cs
+++ b/src/tests/ReactiveUI.Primitives.Tests/SequencerTests.Pools.cs
@@ -160,24 +160,55 @@ public async Task SynchronizationContextSequencerPostsDelayedWorkOnceItIsDue()
await Assert.That(context.PostCount).IsEqualTo(1);
}
- /// Verifies delayed work cancelled before its due time never reaches the synchronization context.
+ /// Verifies cancellation after enqueue prevents delayed work from reaching the dispatcher.
/// A task representing the asynchronous operation.
[Test]
public async Task SynchronizationContextSequencerDropsDelayedWorkCancelledBeforeItIsDue()
{
RecordingSynchronizationContext context = new();
- SynchronizationContextSequencer sequencer = new(context);
+ ControlledDelaySequencer delay = new();
+ SynchronizationContextSequencer sequencer = new(context, delay);
CancellableWorkItem item = new();
- sequencer.Schedule(item, Sequencer.AddTimestamp(sequencer.Timestamp, CancelledDueTime));
- item.Dispose();
+ sequencer.Schedule(item, long.MaxValue);
+ await Assert.That(delay.ScheduledTimestamp).IsEqualTo(long.MaxValue);
+ await Assert.That(delay.Pending).IsNotNull();
+ await Assert.That(context.PostCount).IsEqualTo(0);
- await Task.Delay(CancelObservationWindow);
+ item.Dispose();
+ delay.ExecutePending();
await Assert.That(item.ExecuteCount).IsEqualTo(0);
await Assert.That(context.PostCount).IsEqualTo(0);
}
+ /// Verifies pending delayed work reaches the dispatcher when it has not been cancelled.
+ /// A task representing the asynchronous operation.
+ [Test]
+ public async Task SynchronizationContextSequencerPostsPendingDelayedWork()
+ {
+ RecordingSynchronizationContext context = new();
+ ControlledDelaySequencer delay = new();
+ SynchronizationContextSequencer sequencer = new(context, delay);
+ CancellableWorkItem item = new();
+
+ sequencer.Schedule(item, long.MaxValue);
+ await Assert.That(context.PostCount).IsEqualTo(0);
+ await Assert.That(item.ExecuteCount).IsEqualTo(0);
+
+ delay.ExecutePending();
+
+ await Assert.That(context.PostCount).IsEqualTo(1);
+ await Assert.That(item.ExecuteCount).IsEqualTo(1);
+ }
+
+ /// Verifies the internal delayed scheduler dependency cannot be absent.
+ /// A task representing the asynchronous operation.
+ [Test]
+ public async Task SynchronizationContextSequencerRejectsMissingDelayScheduler() =>
+ await Assert.That(static () => new SynchronizationContextSequencer(new RecordingSynchronizationContext(), null!))
+ .ThrowsExactly();
+
///
/// Verifies disposing a thread-pool sequencer twice releases its queued work exactly once and leaves the sequencer
/// closed. The second disposal must be a no-op rather than a second release of work the first disposal already
@@ -248,6 +279,42 @@ public async Task ThreadPoolSequencerDisposeDuringADrainStopsTheDrainRearmingThe
/// The isolated sequencer.
private static ThreadPoolSequencer CreateIsolatedThreadPoolSequencer() => new();
+ /// Holds a delayed item until the test explicitly dispatches it.
+ private sealed class ControlledDelaySequencer : ISequencer
+ {
+ /// Gets the pending delayed callback.
+ public IWorkItem? Pending { get; private set; }
+
+ /// Gets the timestamp forwarded by the dispatcher.
+ public long ScheduledTimestamp { get; private set; }
+
+ ///
+ public DateTimeOffset Now => DateTimeOffset.UnixEpoch;
+
+ ///
+ public long Timestamp => 0;
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Schedule(IWorkItem item) => item.Execute();
+
+ ///
+ public void Schedule(IWorkItem item, long dueTimestamp)
+ {
+ Pending = item;
+ ScheduledTimestamp = dueTimestamp;
+ }
+
+ /// Executes the item after the test has cancelled or inspected pending work.
+ /// No item is pending.
+ public void ExecutePending()
+ {
+ var pending = Pending ?? throw new InvalidOperationException("No delayed work was queued.");
+ Pending = null;
+ pending.Execute();
+ }
+ }
+
/// Work item that counts how many times a sequencer released it.
private sealed class DisposeCountingWorkItem : IWorkItem, IsDisposed
{
From 8290e5da27e3f69acd6c7e289e0f48c448fe051c Mon Sep 17 00:00:00 2001
From: Chris Pulman
Date: Sat, 12 Sep 2026 05:27:54 +0100
Subject: [PATCH 6/6] fix(ci): update coverage collector to avoid abandoned
mutex crashes
- Upgrade Microsoft.Testing.Extensions.CodeCoverage from 18.11.0 to 18.11.2.
- Resolve the SharedBufferReconciler AbandonedMutexException seen in PR189 Ubuntu coverage collection (microsoft/codecoverage#245).
- Keep all test, instrumentation and coverage gates enabled.
Validation: clean Release build and full Primitives TUnit suites pass on net8-net11 with the new collector (909/911/911/911 tests, zero failures or skips). MTP confirms the changed dispatcher retains 100% measured line and branch coverage. This collector version also passed all ten existing OccasionallyConnected PR matrices.
---
src/Directory.Packages.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index d97feb54..03a26a59 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -69,7 +69,7 @@
-
+