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
This commit is contained in:
Joseph Doherty
2026-10-08 00:22:11 -04:00
parent 390c8269a0
commit b36f5644d3
@@ -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 ──────────────────────────────────────────────────────────────────────────
/// <summary>Driver options over the given tags with the span-grouping knobs under test.</summary>
/// <summary>
/// The post-initialise <c>LastSuccessfulRead</c> stamp, taken only once the wall clock has
/// moved past it. The driver stamps <c>DateTime.UtcNow</c> at init and again at the read
/// pass; under CPU contention both can land in the same clock tick, so a strict
/// "<c>LastSuccessfulRead</c> advanced" assertion flaked (#589). Waiting out the tick makes
/// any later stamp strictly greater without a fixed delay.
/// </summary>
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<RawTagEntry> tags, int maxGap) => new()
{
Host = "192.0.2.1",