test(focas): integration case for re-initialise under live wire reads (R07:H13)

Against the focas-mock fixture: the fixed-tree loop is running and reads are in flight
while the driver re-initialises five times. No reader throws and reads keep returning
Good. After shutdown, a fresh driver still connects and reads.

Refs #740
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 14:27:38 -04:00
parent 2724d7dae8
commit 8ffa6178b5
@@ -141,6 +141,86 @@ public sealed class WireBackendCoverageTests
}
}
/// <summary>
/// #740 / R07:H13 — re-initialising a live driver (the fixed-tree loop running, reads in flight over the real
/// wire) swaps the device table instead of clearing it under its readers: no reader throws, reads keep
/// returning Good, and shutdown leaves no session behind (a fresh driver can still connect afterwards).
/// </summary>
[Fact]
public async Task Reinitialise_under_live_reads_swaps_the_device_table()
{
if (_fx.SkipReason is not null) Assert.Skip(_fx.SkipReason);
var ct = TestContext.Current.CancellationToken;
await _fx.LoadProfileAsync("FWLIB64", ct);
await _fx.PatchStateAsync(new
{
macros = new Dictionary<string, object> { ["500"] = new { value = 42000, @decimal = 3 } },
}, ct);
var drv = new FocasDriver(new FocasDriverOptions
{
Devices = [new FocasDeviceOptions(DeviceHost)],
RawTags = FocasRawTags.Entries(
new FocasTagDefinition("Macro500", DeviceHost, "MACRO:500", FocasDataType.Float64, Writable: false)),
Probe = new FocasProbeOptions { Enabled = false },
FixedTree = new FocasFixedTreeOptions
{
Enabled = true,
PollInterval = TimeSpan.FromMilliseconds(50),
ProgramPollInterval = TimeSpan.Zero,
TimerPollInterval = TimeSpan.Zero,
},
}, driverInstanceId: "wire-reinit-swap", clientFactory: new WireFocasClientFactory());
await using (drv)
{
await drv.InitializeAsync("{}", ct);
using var stop = CancellationTokenSource.CreateLinkedTokenSource(ct);
var errors = new ConcurrentQueue<Exception>();
var good = 0;
var reader = Task.Run(async () =>
{
while (!stop.IsCancellationRequested)
{
try
{
_ = drv.GetHostStatuses();
var s = await drv.ReadAsync(["Macro500", $"{DeviceHost}/Identity/SeriesNumber"], CancellationToken.None);
if (s[0].StatusCode == FocasStatusMapper.Good) Interlocked.Increment(ref good);
}
catch (Exception ex) { errors.Enqueue(ex); }
}
}, CancellationToken.None);
for (var i = 0; i < 5; i++)
{
await Task.Delay(150, ct);
await drv.InitializeAsync("{}", ct);
}
await Task.Delay(300, ct);
stop.Cancel();
await reader;
errors.ShouldBeEmpty();
good.ShouldBeGreaterThan(0, "positive control: reads went over the wire between re-initialises");
}
// A fresh driver still connects and reads Good after the re-initialise churn and the shutdown.
var after = new FocasDriver(new FocasDriverOptions
{
Devices = [new FocasDeviceOptions(DeviceHost)],
RawTags = FocasRawTags.Entries(
new FocasTagDefinition("Macro500", DeviceHost, "MACRO:500", FocasDataType.Float64, Writable: false)),
Probe = new FocasProbeOptions { Enabled = false },
}, driverInstanceId: "wire-reinit-after", clientFactory: new WireFocasClientFactory());
await using (after)
{
await after.InitializeAsync("{}", ct);
(await after.ReadAsync(["Macro500"], ct))[0].StatusCode.ShouldBe(FocasStatusMapper.Good);
}
}
/// <summary>Verifies that discover emits device folder and tag variables.</summary>
[Fact]
public async Task Discover_emits_device_folder_and_tag_variables()