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",