From fe2b0614c813b5e9e2c31d73a3c8c27f482791d9 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 7 Oct 2026 23:51:11 -0400 Subject: [PATCH] test(abcip): deterministic transport-switch probe tests The libplctag-path positive control asserted 'timed out', but libplctag's internal timeout equals the probe timeout and races the probe's own cancellation; under CPU load it surfaces as 'ErrorTimeout' instead. Accept either libplctag stall outcome (native never produces either: it fails at once with 'Connection lost'). Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe --- .../AbCipTransportSwitchTests.cs | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipTransportSwitchTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipTransportSwitchTests.cs index 6fb00abb..0ab8c14f 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipTransportSwitchTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipTransportSwitchTests.cs @@ -116,6 +116,21 @@ public sealed class AbCipTransportSwitchTests driver.TagFactory.ShouldBeOfType(); } + /// + /// Positive control for the libplctag path against a listener that accepts TCP and never speaks CIP. + /// libplctag reports the stall two ways that race on the wall clock: the probe's own cancellation + /// ("Probe timed out after Ns") or libplctag's internal timeout, which has the same duration and + /// surfaces as the ErrorTimeout status. Which fires first depends on scheduler load, so both are + /// the libplctag outcome; the native path never produces either (it fails at once with "Connection lost"). + /// + private static void ShouldBeLibplctagTimeout(string? message) + { + message.ShouldNotBeNull(); + (message.Contains("timed out", StringComparison.OrdinalIgnoreCase) + || message.Contains("ErrorTimeout", StringComparison.Ordinal)) + .ShouldBeTrue($"expected the libplctag stall outcome (timed out / ErrorTimeout) but was: {message}"); + } + /// /// The probe's Phase 1 TCP preflight rejects a refused port before any runtime exists, so the native /// path is proven against a listener that accepts TCP and hangs up: only a runtime that really opens @@ -150,7 +165,7 @@ public sealed class AbCipTransportSwitchTests lib.Message!.ShouldNotContain("Connection lost"); // Positive (#574): the libplctag path ran and reported its own outcome (the bounded probe timeout), // not merely "something other than the native text". - lib.Message!.ShouldContain("timed out"); + ShouldBeLibplctagTimeout(lib.Message); } /// @@ -188,7 +203,7 @@ public sealed class AbCipTransportSwitchTests lib.Message!.ShouldNotContain("Connection lost"); // Positive (#574): the libplctag path ran and reported its own outcome (the bounded probe timeout), // not merely "something other than the native text". - lib.Message!.ShouldContain("timed out"); + ShouldBeLibplctagTimeout(lib.Message); } /// The probe deserialises with its own string-enum converter; a bad transport is a config error. @@ -268,7 +283,7 @@ public sealed class AbCipTransportSwitchTests unpinned.Message!.ShouldContain("CIP handshake failed: Connection lost"); pinned.Ok.ShouldBeFalse(); pinned.Message!.ShouldNotContain("Connection lost"); - pinned.Message!.ShouldContain("timed out"); // positive: libplctag's own outcome (#574) + ShouldBeLibplctagTimeout(pinned.Message); // positive: libplctag's own outcome (#574) } /// Gitea #572: browsePageDelayMs parses (default 5 ms, 0 allowed, negative clamps to 0).