fix(focas): PMC coalescing treats any rc refusal as per-tag fallback, skips transient busy memo, one connect per batch (#266)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -132,6 +132,68 @@ public sealed class FocasPmcCoalescingDriverTests
|
||||
await drv.ShutdownAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_refusal_with_an_unmapped_rc_that_maps_to_BadCommunicationError_still_falls_back_per_tag()
|
||||
{
|
||||
var (drv, c) = await StartAsync(null,
|
||||
Tag("A", "R100", FocasDataType.Byte), Tag("B", "R101", FocasDataType.Byte));
|
||||
c.PmcRangeRejectors.Add((_, _, _) => true);
|
||||
c.PmcRangeRejectStatus = FocasStatusMapper.BadCommunicationError; // what an unlisted rc / EW_PARITY / EW_SOCKET maps to
|
||||
c.Values["R100"] = (sbyte)1; c.Values["R101"] = (sbyte)2;
|
||||
|
||||
var r = await drv.ReadAsync(["A", "B"], CancellationToken.None);
|
||||
r.Select(s => s.Value).ShouldBe([(object)(sbyte)1, (sbyte)2]);
|
||||
r.ShouldAllBe(s => s.StatusCode == FocasStatusMapper.Good);
|
||||
|
||||
await drv.ReadAsync(["A", "B"], CancellationToken.None);
|
||||
c.PmcRangeReads.Count.ShouldBe(1, "a deterministic rc refusal is memoised");
|
||||
await drv.ShutdownAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_transient_busy_refusal_is_not_memoised()
|
||||
{
|
||||
var (drv, c) = await StartAsync(null,
|
||||
Tag("A", "R100", FocasDataType.Byte), Tag("B", "R101", FocasDataType.Byte));
|
||||
var busy = true;
|
||||
c.PmcRangeRejectors.Add((_, _, _) => busy);
|
||||
c.PmcRangeRejectStatus = FocasStatusMapper.BadDeviceFailure; // EW_BUSY
|
||||
c.Values["R100"] = (sbyte)1; c.Values["R101"] = (sbyte)2;
|
||||
|
||||
(await drv.ReadAsync(["A", "B"], CancellationToken.None)).ShouldAllBe(s => s.StatusCode == FocasStatusMapper.Good);
|
||||
busy = false;
|
||||
c.PmcMemory[("R", 100)] = 5; c.PmcMemory[("R", 101)] = 6;
|
||||
var r = await drv.ReadAsync(["A", "B"], CancellationToken.None);
|
||||
|
||||
c.PmcRangeReads.Count.ShouldBe(2, "the range is retried once the CNC is no longer busy");
|
||||
r.Select(s => s.Value).ShouldBe([(object)(sbyte)5, (sbyte)6]);
|
||||
await drv.ShutdownAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_down_device_costs_one_connect_attempt_per_batch_not_one_per_plan_and_tag()
|
||||
{
|
||||
var client = new FakeFocasClient { ThrowOnConnect = true };
|
||||
var drv = new FocasDriver(new FocasDriverOptions
|
||||
{
|
||||
Devices = [new FocasDeviceOptions(Host)],
|
||||
Probe = new FocasProbeOptions { Enabled = false },
|
||||
RawTags = FocasRawTags.Entries(new[]
|
||||
{
|
||||
Tag("A", "R100", FocasDataType.Byte), Tag("B", "R101", FocasDataType.Byte),
|
||||
Tag("C", "R900", FocasDataType.Byte), Tag("D", "R2000", FocasDataType.Byte),
|
||||
}),
|
||||
}, "drv-1", new FakeFocasClientFactory { Customise = () => client });
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
var before = client.ConnectCount;
|
||||
|
||||
var r = await drv.ReadAsync(["A", "B", "C", "D"], CancellationToken.None);
|
||||
|
||||
r.ShouldAllBe(s => s.StatusCode == FocasStatusMapper.BadCommunicationError);
|
||||
(client.ConnectCount - before).ShouldBe(1);
|
||||
await drv.ShutdownAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_transport_failure_fails_every_member_without_per_tag_retries()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user