using System.Diagnostics; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests; /// /// Review 07 P7 (High), wire level: one driver serving a live ab_server device AND a dead device (a /// blackholed address, so every connect waits out the full timeout). Before the fix the dead device's 20 tags /// cost 20 x Timeout per poll and the live device was read once per cycle; now the live device reads Good in /// the same call, the dead device costs at most two timeouts, and the next poll does not touch it at all. /// Read-only. /// [Collection(AbLegacyServerCollection.Name)] [Trait("Category", "Integration")] [Trait("Simulator", "ab_server-PCCC")] public sealed class AbLegacyDeadDeviceIsolationTests(AbLegacyServerFixture sim) { // TEST-NET-1 (RFC 5737): never routed, so a connect blackholes until the driver's timeout. private const string DeadUri = "ab://192.0.2.1:44818/1,0"; /// The live device is served promptly while a blackholed sibling is throttled. /// The simulator profile. /// The transport stack the driver runs on. [AbLegacyTheory] [MemberData(nameof(TransportMatrix.Profiles), MemberType = typeof(TransportMatrix))] public async Task Dead_device_does_not_stall_the_live_one(AbLegacyServerProfile profile, AbLegacyTransport transport) { if (sim.SkipReasonFor(profile) is { } skip) Assert.Skip(skip); var liveUri = $"ab://{sim.Host}:{sim.PortFor(profile)}/{sim.CipPath}"; var timeout = TimeSpan.FromSeconds(2); var dead = Enumerable.Range(0, 20) .Select(i => new AbLegacyTagDefinition($"Dead{i}", DeadUri, $"N7:{i}", AbLegacyDataType.Int)) .ToArray(); await using var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions(liveUri, profile.Family), new AbLegacyDeviceOptions(DeadUri, profile.Family)], RawTags = AbLegacyRawTags.Entries( [new AbLegacyTagDefinition("Live", liveUri, "N7:0", AbLegacyDataType.Int), .. dead]), Timeout = timeout, Probe = new AbLegacyProbeOptions { Enabled = false }, Transport = transport, }, driverInstanceId: $"ablegacy-p7-{profile.Family}-{transport}"); var ct = TestContext.Current.CancellationToken; await drv.InitializeAsync("{}", ct); string[] refs = ["Live", .. dead.Select(d => d.Name)]; // Two consecutive comms faults mark the device down. With coalescing on, the dead device's 20 tags are ONE // block (one fault per poll), so the window can open on the second poll rather than the first — either way // at most two timeouts are paid before the device stops being dialled. var total = TimeSpan.Zero; TimeSpan last = default; for (var poll = 0; poll < 3; poll++) { var sw = Stopwatch.StartNew(); var r = await drv.ReadAsync(refs, ct); sw.Stop(); total += last = sw.Elapsed; r[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good, $"poll {poll}: the live device must be served in the same poll"); r.Skip(1).ShouldAllBe(s => s.StatusCode != AbLegacyStatusMapper.Good); } total.ShouldBeLessThan(timeout * 2.5, "a dead device may cost at most two timeouts per backoff window"); last.ShouldBeLessThan(timeout, "inside the backoff window the dead device is not dialled"); drv.GetHealth().State.ShouldBe(DriverState.Degraded); } }