diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Series/WireBackendCoverageTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Series/WireBackendCoverageTests.cs index 4cbf460e..ae0055b3 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Series/WireBackendCoverageTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Series/WireBackendCoverageTests.cs @@ -141,6 +141,86 @@ public sealed class WireBackendCoverageTests } } + /// + /// #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). + /// + [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 { ["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(); + 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); + } + } + /// Verifies that discover emits device folder and tag variables. [Fact] public async Task Discover_emits_device_folder_and_tag_variables()