From b36f5644d351c32e04a4afdc926eae9e4cfeb7dc Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 8 Oct 2026 00:22:11 -0400 Subject: [PATCH] test(s7): wait out the clock tick before asserting LastSuccessfulRead advanced (#589) The driver stamps DateTime.UtcNow at init and again per read pass; under CPU contention both land in the same tick, so the strict greater-than assertions flaked (reproduced 1/40 under load, sibling test A_cancelled_pass_still_flushes... failed identically with equal timestamps). Test-only race; no production defect. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe --- .../S7SpanReadParityTests.cs | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7SpanReadParityTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7SpanReadParityTests.cs index d6d752c8..2dc2152f 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7SpanReadParityTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7SpanReadParityTests.cs @@ -451,7 +451,7 @@ public sealed class S7SpanReadParityTests // The connect stamped a LastSuccessfulRead; the pass must move it PAST that value, so a // frozen timestamp is distinguishable from a merely non-null one. - var before = driver.GetHealth().LastSuccessfulRead; + var before = BaselineStamp(driver); before.ShouldNotBeNull(); var results = await driver.ReadAsync(ScatteredRefs(3), ct); @@ -502,7 +502,7 @@ public sealed class S7SpanReadParityTests using var driver = new S7Driver(Options(tags, maxGap: 16), "s7-span-decode-fault", factory); await driver.InitializeAsync("{}", ct); - var before = driver.GetHealth().LastSuccessfulRead; + var before = BaselineStamp(driver); var results = await driver.ReadAsync(["BadStr", "GoodWord"], ct); // ONE block read served both members — this really is the span path, not two per-tag reads. @@ -548,7 +548,7 @@ public sealed class S7SpanReadParityTests using var driver = new S7Driver(Options(NarrowTags(), maxGap: -1), "s7-access-denied", factory); await driver.InitializeAsync("{}", ct); - var before = driver.GetHealth().LastSuccessfulRead; + var before = BaselineStamp(driver); var results = await driver.ReadAsync(["A", "B", "C"], ct); results[0].StatusCode.ShouldNotBe(0u); @@ -589,7 +589,7 @@ public sealed class S7SpanReadParityTests using var driver = new S7Driver(Options(NarrowTags(), maxGap: -1), "s7-escalation-message", factory); await driver.InitializeAsync("{}", ct); - var before = driver.GetHealth().LastSuccessfulRead; + var before = BaselineStamp(driver); var results = await driver.ReadAsync(["A", "B", "C"], ct); results[0].StatusCode.ShouldBe(StatusBadDeviceFailure); @@ -632,7 +632,7 @@ public sealed class S7SpanReadParityTests using var driver = new S7Driver(Options(tags, maxGap: -1), "s7-cancel-flush", factory); await driver.InitializeAsync("{}", ct); - var before = driver.GetHealth().LastSuccessfulRead; + var before = BaselineStamp(driver); using var cts = new CancellationTokenSource(); var reading = driver.ReadAsync(["Narrow", "Wide"], cts.Token); @@ -802,6 +802,21 @@ public sealed class S7SpanReadParityTests // ── Fixture ────────────────────────────────────────────────────────────────────────── /// Driver options over the given tags with the span-grouping knobs under test. + /// + /// The post-initialise LastSuccessfulRead stamp, taken only once the wall clock has + /// moved past it. The driver stamps DateTime.UtcNow at init and again at the read + /// pass; under CPU contention both can land in the same clock tick, so a strict + /// "LastSuccessfulRead advanced" assertion flaked (#589). Waiting out the tick makes + /// any later stamp strictly greater without a fixed delay. + /// + private static DateTime? BaselineStamp(S7Driver driver) + { + var stamp = driver.GetHealth().LastSuccessfulRead; + if (stamp is { } s) + SpinWait.SpinUntil(() => DateTime.UtcNow > s); + return stamp; + } + private static S7DriverOptions Options(IReadOnlyList tags, int maxGap) => new() { Host = "192.0.2.1",