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:
+80
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user