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
19 changes: 15 additions & 4 deletions tests/CanKit.Pro.Tests/Infrastructure/ClockedCanBusService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,28 @@ internal sealed class ClockedCanBusService : ICanBusService
private readonly CanBusService _inner;
private readonly AsyncLocal<SendDeadline?> _deadline = new();
private readonly object _sendGate;
private readonly bool _ownsTimers;
private int _pending;
private int _disposed;

public ClockedCanBusService(ICanBus bus, ManualTimeSource clock, object? sendGate = null)
: this(bus, new ProtocolActor(ActorExecutionMode.DedicatedThread, null, clock, null),
ownsTimers: true, sendGate)
{ }

/// <summary>The caller's clock owns this actor and may keep advancing it after service disposal.</summary>
public ClockedCanBusService(ICanBus bus, ProtocolActor timers)
: this(bus, timers, ownsTimers: false, sendGate: null) { }

private ClockedCanBusService(ICanBus bus, ProtocolActor timers, bool ownsTimers, object? sendGate)
{
_sendGate = sendGate ?? new object();
Timers = new ProtocolActor(ActorExecutionMode.DedicatedThread, null, clock, null);
try { _inner = new CanBusService(bus, clock.GetTimestamp); }
_ownsTimers = ownsTimers;
Timers = timers;
try { _inner = new CanBusService(bus, timers.TimeSource.GetTimestamp); }
catch
{
Timers.Dispose();
if (_ownsTimers) Timers.Dispose();
throw;
}
_inner.ArmConfirmTimeout = (source, timeout) =>
Expand Down Expand Up @@ -90,7 +101,7 @@ public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0) return;
try { _inner.Dispose(); }
finally { Timers.Dispose(); }
finally { if (_ownsTimers) Timers.Dispose(); }
}

private sealed class SendDeadline : IDisposable
Expand Down
45 changes: 45 additions & 0 deletions tests/CanKit.Pro.Tests/TestCases/ClockedCanBusServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,51 @@ public async Task A_Confirmation_Removed_After_Timer_Selection_Cannot_Advance_Pr
bench.Now.Should().Be(started, "an already removed confirmation deadline cannot choose a clock step");
}

[Fact]
public async Task Disposing_A_Service_Preserves_Its_Clock_Owned_Actor()
{
using var bus = ControllableBus.DeferredEchoCapable(VirtualAdapterFixture.NewSession("borrowed-confirm"));
using var clock = new VirtualClock();
var actor = clock.NewActor();
Task<TxConfirmation> send;
using (var service = new ClockedCanBusService(bus, actor))
{
send = service.SendConfirmedAsync(CanFrame.Classic(0x123, new byte[] { 1 }));
await bus.DeferredEchoes.WaitForEnqueuedAsync(1, HangGuard);
await ArmedAsync(service, TimeSpan.FromSeconds(1));
}
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => send.WaitAsync(HangGuard));
(await actor.NextTimerDelayAsync()).Should().BeNull("service disposal removes its own pending deadline");
bool fired = false;
using var timer = actor.Schedule(TimeSpan.FromMilliseconds(10), () => fired = true);
await clock.AdvanceAsync(TimeSpan.FromMilliseconds(10));
fired.Should().BeTrue("the external clock still owns and drives its actor");
}

[Fact]
public async Task Confirmation_Deadlines_Do_Not_Shadow_The_Protocol_Actors_Timer()
{
using var bus = ControllableBus.DeferredEchoCapable(VirtualAdapterFixture.NewSession("separate-confirm"));
using var clock = new VirtualClock();
var protocol = clock.NewActor();
using var service = new ClockedCanBusService(bus, clock.NewActor());
bool protocolFired = false;
using var timer = protocol.Schedule(TimeSpan.FromSeconds(1), () => protocolFired = true);
var budget = TimeSpan.FromMilliseconds(500);
var send = service.SendConfirmedAsync(CanFrame.Classic(0x123, new byte[] { 1 }), budget);
await bus.DeferredEchoes.WaitForEnqueuedAsync(1, HangGuard);
await ArmedAsync(service, budget);
(await protocol.NextTimerDelayAsync()).Should().Be(TimeSpan.FromSeconds(1));
await clock.AdvanceAsync(budget - TimeSpan.FromTicks(1));
send.IsCompleted.Should().BeFalse();
await clock.AdvanceAsync(TimeSpan.FromTicks(1));
(await send.WaitAsync(HangGuard)).FailureReason.Should().Be(TxConfirmFailureReason.Timeout);
protocolFired.Should().BeFalse();
(await protocol.NextTimerDelayAsync()).Should().Be(budget);
await clock.AdvanceAsync(budget);
protocolFired.Should().BeTrue();
}

private static async Task ArmedAsync(ClockedCanBusService service, TimeSpan delay)
{
var until = DateTime.UtcNow + HangGuard;
Expand Down
28 changes: 25 additions & 3 deletions tests/CanKit.Pro.Tests/TestCases/Uds/UdsFunctionalClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,25 @@ private sealed class WindowClock : IDisposable
{
public VirtualClock Clock { get; } = new();
public ProtocolActor Actor { get; }
public ProtocolActor ConfirmationActor { get; }

public WindowClock()
{
Actor = Clock.NewActor();
ConfirmationActor = Clock.NewActor();
Clock.Advance(TimeSpan.FromMilliseconds(1));
}

public long Now => Actor.TimeSource.GetTimestamp();
public ClockedCanBusService OpenService(ICanBus bus) => new(bus, ConfirmationActor);

public void Dispose() => Clock.Dispose();
}

private static UdsFunctionalClient OpenOnClock(ICanBus bus, WindowClock clock,
TimeSpan? responseWindow = null, TimeSpan? responsePendingWindow = null,
IsoTpFunctionalOptions? options = null)
=> OpenOnClock(new CanBusService(bus, clock.Actor.TimeSource.GetTimestamp), clock,
=> OpenOnClock(clock.OpenService(bus), clock,
responseWindow, responsePendingWindow, options);

private static UdsFunctionalClient OpenOnClock(ICanBusService service, WindowClock clock,
Expand All @@ -83,6 +86,25 @@ private static UdsFunctionalClient OpenOnClock(ICanBusService service, WindowClo
responseWindow: responseWindow, responsePendingWindow: responsePendingWindow);
}

[Fact]
public async Task A_Missing_Functional_Echo_Expires_NAs_On_The_Window_Clock()
{
using var clock = new WindowClock();
using var bus = ControllableBus.DeferredEchoCapable(NewSession());
var options = FastOptions();
var p2 = TimeSpan.FromSeconds(1);
using var functional = OpenOnClock(bus, clock, responseWindow: p2, options: options);
var call = functional.SendRawAsync(new byte[] { 0x22, 0xF1, 0x90 }, ShortCollection);
await bus.DeferredEchoes.WaitForEnqueuedAsync(1, ShortTimeout);
await clock.Clock.WaitUntilTimerArmedAsync(clock.ConfirmationActor, options.NAs, ShortTimeout);
await WaitForProvisionalWindow(clock, p2);
await clock.Clock.AdvanceAsync(options.NAs - TimeSpan.FromTicks(1));
call.IsCompleted.Should().BeFalse("N_As still has one virtual tick left");
await clock.Clock.AdvanceAsync(TimeSpan.FromTicks(1));
var failure = await Assert.ThrowsAsync<IsoTpTimeoutException>(() => call.WaitAsync(ShortTimeout));
failure.Timer.Should().Be(IsoTpTimer.NAs);
}

private static async Task WaitUntilArmed(WindowClock clock, TimeSpan expected, TimeSpan? lateBy = null)
=> await clock.Clock.WaitUntilTimerArmedAsync(clock.Actor, expected, ShortTimeout,
lateBy ?? TimeSpan.FromMilliseconds(5));
Expand Down Expand Up @@ -937,7 +959,7 @@ public async Task The_Pending_Answers_That_Move_A_Window_Out_Are_Bounded()
using var busEcus = OpenClassic(session, 1);
var pending = SingleFrameFrom(Ecu1, new byte[] { 0x7F, 0x3E, 0x78 });

var iso = new IsoTpFunctionalClient(new CanBusService(busTester, clock.Actor.TimeSource.GetTimestamp),
var iso = new IsoTpFunctionalClient(clock.OpenService(busTester),
FunctionalTxId, Ecu1, 0x7EF, FastOptions(), ownsService: true, clock.Actor);
using var functional = UdsFunctionalClient.Create(iso, clock.Actor, ownsClient: true,
responseWindow: TimeSpan.FromMilliseconds(300), responsePendingWindow: TimeSpan.FromMilliseconds(1500),
Expand Down Expand Up @@ -1010,7 +1032,7 @@ public async Task A_Pending_Answer_From_Before_The_Handoff_Is_Not_This_Requests(
var p2 = TimeSpan.FromMilliseconds(windowMs);
using var clock = new WindowClock();
using var bus = ControllableBus.DeferredEchoCapable(NewSession());
using var inner = new CanBusService(bus, clock.Actor.TimeSource.GetTimestamp);
using var inner = clock.OpenService(bus);
var service = new SendEnteredService(inner);
using var functional = OpenOnClock(service, clock,
responseWindow: p2, responsePendingWindow: TimeSpan.FromMilliseconds(3000));
Expand Down
Loading