From 2bea4bc6af6ace6352f50330011cf3a8137eb7db Mon Sep 17 00:00:00 2001 From: Chris Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com> Date: Sun, 27 Sep 2026 09:17:39 -0400 Subject: [PATCH 1/2] test(concurrency): clear the seven InspectCode alerts in ConcurrencyStressTests - AccessToModifiedClosure: the invocation counters are StrongBox, so the lambdas capture a reference that never changes; the count is only touched through Interlocked / Volatile on .Value. - AsyncFixer06: Record.ExceptionAsync(async () => await call) instead of an implicit Task -> Task lambda conversion. - xUnit1031: no blocking call.Result; the completed call is awaited once (only when it did not throw) and the assertion reads that value. Same invariants; both race outcomes still observed. Co-Authored-By: Claude Opus 5.5 --- .../ConcurrencyStressTests.cs | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/tests/Wolfgang.TryPattern.Tests.Concurrency/ConcurrencyStressTests.cs b/tests/Wolfgang.TryPattern.Tests.Concurrency/ConcurrencyStressTests.cs index 4a2d5034..0ef69fbf 100644 --- a/tests/Wolfgang.TryPattern.Tests.Concurrency/ConcurrencyStressTests.cs +++ b/tests/Wolfgang.TryPattern.Tests.Concurrency/ConcurrencyStressTests.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Xunit.Abstractions; @@ -130,12 +131,15 @@ public async Task RunAsync_Action_when_cancellation_races_scheduling_a_canceled_ for (var round = 0; round < StressIterations; round++) { using var cts = new CancellationTokenSource(); - var runs = 0; + // A box, not a local int: the closure captures a reference that never + // changes, and the count itself is only touched through Interlocked/Volatile. + var runs = new StrongBox(); var cancel = Task.Run(cts.Cancel); - var call = Try.RunAsync(() => Interlocked.Increment(ref runs), cts.Token); - var thrown = await Record.ExceptionAsync(() => call); + var call = Try.RunAsync(() => Interlocked.Increment(ref runs.Value), cts.Token); + var thrown = await Record.ExceptionAsync(async () => await call); await cancel; + Result? result = thrown is null ? await call : null; // Exactly two legal outcomes: canceled before Task.Run started the // action (OperationCanceledException, action never ran), or the @@ -143,9 +147,9 @@ public async Task RunAsync_Action_when_cancellation_races_scheduling_a_canceled_ Assert.True ( thrown is OperationCanceledException - ? Volatile.Read(ref runs) == 0 - : thrown is null && call.Result.Succeeded && Volatile.Read(ref runs) == 1, - $"round {round}: thrown={thrown?.GetType().Name ?? "none"}, runs={runs}" + ? Volatile.Read(ref runs.Value) == 0 + : result is { Succeeded: true } && Volatile.Read(ref runs.Value) == 1, + $"round {round}: thrown={thrown?.GetType().Name ?? "none"}, runs={Volatile.Read(ref runs.Value)}" ); canceled += thrown is null ? 0 : 1; } @@ -162,20 +166,21 @@ public async Task RunAsync_Func_when_cancellation_races_the_token_check_a_cancel for (var round = 0; round < StressIterations; round++) { using var cts = new CancellationTokenSource(); - var invocations = 0; + var invocations = new StrongBox(); var cancel = Task.Run(cts.Cancel); var call = Try.RunAsync ( () => { - Interlocked.Increment(ref invocations); + Interlocked.Increment(ref invocations.Value); return Task.FromResult(42); }, cts.Token ); - var thrown = await Record.ExceptionAsync(() => call); + var thrown = await Record.ExceptionAsync(async () => await call); await cancel; + Result? result = thrown is null ? await call : null; // Canceled before the token check: OperationCanceledException and the // function was never invoked. Otherwise it was invoked exactly once and @@ -183,9 +188,9 @@ public async Task RunAsync_Func_when_cancellation_races_the_token_check_a_cancel Assert.True ( thrown is OperationCanceledException - ? Volatile.Read(ref invocations) == 0 - : thrown is null && call.Result.Succeeded && call.Result.Value == 42 && Volatile.Read(ref invocations) == 1, - $"round {round}: thrown={thrown?.GetType().Name ?? "none"}, invocations={invocations}" + ? Volatile.Read(ref invocations.Value) == 0 + : result is { Succeeded: true, Value: 42 } && Volatile.Read(ref invocations.Value) == 1, + $"round {round}: thrown={thrown?.GetType().Name ?? "none"}, invocations={Volatile.Read(ref invocations.Value)}" ); canceled += thrown is null ? 0 : 1; } From a3236806cfba442acc2172b2b16807b5d0d3b7e1 Mon Sep 17 00:00:00 2001 From: Chris Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com> Date: Sun, 27 Sep 2026 13:16:48 -0400 Subject: [PATCH 2/2] test: make the mid-flight cancellation test's coverage timing-independent #383's reorder only narrowed the race: on macOS ARM64 the cancel could still land between TrySetResult and ThrowIfCancellationRequested, so the `while (true)` loop-back never ran and RunAsyncActionTests measured 98.9% (PR #391, run 36322172822, Stage 3). The action is now straight-line: signal started, block on the token's WaitHandle (10 s timeout), record whether cancellation was seen, then an unconditional `throw` of OperationCanceledException(token). No loop, and no reachable normal exit for the compiler to emit a closing-brace sequence point for. The test still proves the action was running, observed the cancellation (WaitOne returns true only once the token is cancelled, now asserted), and that RunAsync propagated the OperationCanceledException. Co-Authored-By: Claude Opus 5.5 --- .../RunAsyncActionTests.cs | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/tests/Wolfgang.TryPattern.Tests.Unit/RunAsyncActionTests.cs b/tests/Wolfgang.TryPattern.Tests.Unit/RunAsyncActionTests.cs index dc6d8a72..3e22b80c 100644 --- a/tests/Wolfgang.TryPattern.Tests.Unit/RunAsyncActionTests.cs +++ b/tests/Wolfgang.TryPattern.Tests.Unit/RunAsyncActionTests.cs @@ -245,26 +245,19 @@ public async Task RunAsync_Action_CancellationToken_when_cancellation_is_request var actionStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var cancellationObserved = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + // Straight-line, ending in an unconditional `throw`, so every line runs in + // every interleaving and the compiler emits no normal-exit sequence point. + // A polling loop left a line (the first delay, later the loop-back) + // uncovered whenever a fast runner cancelled early, and a trailing + // ThrowIfCancellationRequested left the closing brace uncovered; either + // fails the 100% test-assembly gate. WaitOne returns true only once the + // token is cancelled; the timeout turns a cancellation that never + // arrives into a failed assertion instead of a hang. void Action() { - try - { - // Signal "started" only after a full pass of the loop body, so - // every line runs before the test can cancel. Signalling first - // let a fast runner cancel before the first delay, leaving that - // line uncovered on some runs (97.9% on macOS ARM64). - while (true) - { - Task.Delay(10).Wait(); - actionStarted.TrySetResult(true); - cts.Token.ThrowIfCancellationRequested(); - } - } - catch (OperationCanceledException) - { - cancellationObserved.TrySetResult(true); - throw; - } + actionStarted.TrySetResult(true); + cancellationObserved.TrySetResult(cts.Token.WaitHandle.WaitOne(TimeSpan.FromSeconds(10))); + throw new OperationCanceledException(cts.Token); } var task = Try.RunAsync(Action, cts.Token); @@ -274,7 +267,6 @@ void Action() cts.Cancel(); await Assert.ThrowsAsync(() => task); - var completedTask = await Task.WhenAny(cancellationObserved.Task, Task.Delay(1000)); - Assert.Same(cancellationObserved.Task, completedTask); + Assert.True(await cancellationObserved.Task, "the action never observed the cancellation"); } }