From 89bb779f9283ea63484d755288da434ee63dcb4e Mon Sep 17 00:00:00 2001 From: Dietmar Borgards <2646931+dborgards@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:39:08 +0100 Subject: [PATCH] test(isotp): pin the documented reserved FlowStatus deadline After one CTS-controlled block, send reserved FS 3 and 15 while N_Bs is already running. Verify that neither aborts nor re-arms the send, and that the original virtual deadline still ends it with an N_Bs timeout. This characterizes the documented current contract; the ISO 15765-2 norm question in #353 remains open. No production behaviour is changed. Mutation checks (compiled, runtime-red, sources restored): - Decode reserved FS as Wait: both rows fail, 500 ms instead of 300 ms. - Decode reserved FS as Overflow: both rows fail on premature completion. Refs #353 --- .../IsoTp/IsoTpChannelIntegrationTests.cs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/CanKit.Pro.Tests/TestCases/IsoTp/IsoTpChannelIntegrationTests.cs b/tests/CanKit.Pro.Tests/TestCases/IsoTp/IsoTpChannelIntegrationTests.cs index e5d8ec42..04db3ea7 100644 --- a/tests/CanKit.Pro.Tests/TestCases/IsoTp/IsoTpChannelIntegrationTests.cs +++ b/tests/CanKit.Pro.Tests/TestCases/IsoTp/IsoTpChannelIntegrationTests.cs @@ -496,6 +496,68 @@ public async Task A_Single_Wait_Rearms_NBs_And_Silence_After_It_Times_Out() (await act.Should().ThrowAsync()).Which.Timer.Should().Be(IsoTpTimer.NBs); } + // -------------------------------------------------------------------------------- + // #353: characterize the documented drop of reserved FS during an active transfer. + // This pins the current contract, not a reading of ISO 15765-2; the normative question + // remains open. Moving virtual time before the reserved FC distinguishes a drop from a + // Wait that would silently re-arm N_Bs and extend the transfer. + // -------------------------------------------------------------------------------- + [Theory] + [InlineData(3)] + [InlineData(15)] + public async Task A_Reserved_FlowStatus_After_A_Block_Leaves_The_Original_NBs_Deadline(int flowStatus) + { + var session = NewSession(); + using var busA = OpenClassic(session, 0); + using var busB = OpenClassic(session, 1); + var epAB = IsoTpEndpoint.Normal(0x420, 0x421); + var epBA = IsoTpEndpoint.Normal(0x421, 0x420); + var nBs = TimeSpan.FromMilliseconds(500); + using var clock = new VirtualClock(); + using var service = new CanBusService(busA); + var senderActor = clock.NewActor(); + using var sender = new IsoTpChannel(service, epAB, FastOptions(nBs: nBs), + ownsService: false, senderActor); + using var cfArrived = new SemaphoreSlim(0); + using var fcArrived = new SemaphoreSlim(0); + busB.FrameObserved += (_, e) => + { + var data = e.CanFrame.Data.ToArray(); + if (e.CanFrame.ID == 0x420 && data.Length > 0 && (data[0] >> 4) == 0x2) + cfArrived.Release(); + }; + busA.FrameObserved += (_, e) => + { + var data = e.CanFrame.Data.ToArray(); + if (e.CanFrame.ID == 0x421 && data.Length > 0 && (data[0] >> 4) == 0x3) + fcArrived.Release(); + }; + + var sendTask = sender.SendAsync(Enumerable.Range(0, 27).Select(i => (byte)i).ToArray()); + await clock.WaitUntilTimerArmedAsync(senderActor, nBs, ShortTimeout); + var fc = IsoTpFrameCodec.BuildFlowControl(epBA, FlowStatus.ClearToSend, + blockSize: 1, stMinRaw: 0, isCanFd: false, padding: true); + busB.Transmit(CanFrame.Classic(0x421, fc)); + (await fcArrived.WaitAsync(ShortTimeout)).Should().BeTrue(); + (await cfArrived.WaitAsync(ShortTimeout)).Should().BeTrue(); + await clock.WaitUntilTimerArmedAsync(senderActor, nBs, ShortTimeout); + await clock.AdvanceAsync(TimeSpan.FromMilliseconds(200)); + + fc[0] = (byte)(0x30 | flowStatus); + busB.Transmit(CanFrame.Classic(0x421, fc)); + (await fcArrived.WaitAsync(ShortTimeout)).Should().BeTrue(); + await sender.SettleAsync().WaitAsync(ShortTimeout); + sendTask.IsCompleted.Should().BeFalse("a reserved FS neither completes nor aborts the active send"); + (await senderActor.NextTimerDelayAsync()).Should().Be(TimeSpan.FromMilliseconds(300), + "a dropped reserved FS must not re-arm N_Bs as Wait would"); + + await clock.AdvanceAsync(TimeSpan.FromMilliseconds(299)); + sendTask.IsCompleted.Should().BeFalse("the original deadline has not elapsed"); + await clock.AdvanceAsync(TimeSpan.FromMilliseconds(2)); + Func act = () => sendTask.WaitAsync(ShortTimeout); + (await act.Should().ThrowAsync()).Which.Timer.Should().Be(IsoTpTimer.NBs); + } + // -------------------------------------------------------------------------------- // FR-TP-012 — Overflow FC aborts the send with a reported failure. // --------------------------------------------------------------------------------