From 13398f8e6a80b2ef41ec892290e6dfae0139a488 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 20:37:13 +0000 Subject: [PATCH] test(uds): take R's reference before the cancellation, not after the throw R measured the second send's gap from a timestamp read once the test had observed the cancelled send's exception -- after the client's note, by however long the test's continuation took to be scheduled. Every millisecond of that came off the gap, so the 5 ms margin was a bound on host scheduling. Under a 4-process CPU load on a 4-CPU container it failed 14 runs in 20, short by up to 13 ms. The send is now cancelled by hand and the reference is read just before Cancel(), which is no later than the note the cancellation causes: delay can only lengthen the measured gap. A precondition asserts the provisional window was over at cancellation. 30 of 30 under the same load; with the re-note in the send's catch removed it fails 5 of 5 (gap ~18 ms against the 75 ms floor). Refs #92 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Kv8GdFfMkktfWdqjNyCDtW --- .../TestCases/Uds/UdsExpiredDeadlineTests.cs | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/CanKit.Pro.Tests/TestCases/Uds/UdsExpiredDeadlineTests.cs b/tests/CanKit.Pro.Tests/TestCases/Uds/UdsExpiredDeadlineTests.cs index a2cf32b..33fc104 100644 --- a/tests/CanKit.Pro.Tests/TestCases/Uds/UdsExpiredDeadlineTests.cs +++ b/tests/CanKit.Pro.Tests/TestCases/Uds/UdsExpiredDeadlineTests.cs @@ -442,23 +442,34 @@ public async Task R_A_Suppressed_Send_Cancelled_After_Its_Provisional_Window_Sti // Cancelled at 150 ms: past the 80 ms window noted before the send, with the send // still held (macOS CI on #150 fired a 150 ms timer after a 300 ms send had completed). - using var early = new CancellationTokenSource(TimeSpan.FromMilliseconds(150)); - Func cancelled = () => client.SendRawAsync(new byte[] { 0x3E, 0x80 }, early.Token); - await cancelled.Should().ThrowAsync(); + using var early = new CancellationTokenSource(); + var pending = client.SendRawAsync(new byte[] { 0x3E, 0x80 }, early.Token); + await Task.Delay(TimeSpan.FromMilliseconds(150)); + // Read before the cancellation, so no later than the client's note it causes. Read + // after the throw was observed, as it once was, it lagged the note by however long the + // test's continuation took to be scheduled -- 13 ms under load on a 4-CPU runner, 14 + // runs in 20 -- and every millisecond of that came off the gap. var cancelledAt = Stopwatch.GetTimestamp(); + early.Cancel(); + Func cancelled = () => pending; + await cancelled.Should().ThrowAsync(); using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(500)); Func next = () => client.TesterPresentAsync(suppressPositiveResponse: false, cts.Token); await next.Should().ThrowAsync(); // the stub never answers; what matters is when it sent - long gapTicks; + long heldTicks, gapTicks; lock (channel.Sent) { channel.Sent.Should().HaveCount(2); + heldTicks = cancelledAt - channel.Sent[0].StartedAt; gapTicks = channel.Sent[1].StartedAt - cancelledAt; } - // The second send waited P2 from the cancellation, less a margin for the note preceding - // the throw; with only the pre-send window, already over, it would go out at once. + TimeSpan.FromSeconds((double)heldTicks / Stopwatch.Frequency).Should().BeGreaterThan(Budget, + "the provisional window must be over when the send is cancelled, or this is test O"); + // The second send waited P2 from the cancellation; with only the pre-send window, already + // over, it would go out at once. Scheduling delay can only lengthen the gap now, never + // shorten it, so the margin -- O's and Q's -- no longer has to absorb it. TimeSpan.FromSeconds((double)gapTicks / Stopwatch.Frequency).Should().BeGreaterThanOrEqualTo( Budget - TimeSpan.FromMilliseconds(5), "the cancelled send's window was noted again from the cancellation");