From 98c9458830853070e769708fbe11ddb84d4b145b Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 15 Aug 2026 21:16:55 -0400 Subject: [PATCH] =?UTF-8?q?test(transport):=20row=2074=20=E2=80=94=20gauge?= =?UTF-8?q?=20asserts=20become=20delta-over-scoped-action,=20not=20absolut?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BundleSessionObservabilityTests' two gauge facts asserted absolute values (Assert.Equal(0, baseline), Assert.Equal(2, ReadGauge())) against what is a single process-global provider slot — the same latent shape as the row-32 QueueDepthGaugeTests flake, correct only while this file is the slot's sole registrant. Converted to the delta-over-scoped-action pattern the row-66 rejection-counter facts in the same file already use: a new ReadGaugeDelta helper measures how far the instrument MOVED across an action driven by this test's own store, so an inherited non-zero baseline cancels out. Two assertions got stronger in the process. The post-clear check was Assert.Equal(0, ReadGauge()) — "the instrument reports zero", which any unregistered slot satisfies; it is now "opening more sessions on the detached store moves the gauge by nothing", which is the actual claim. And the compare-and-clear fact gained a negative control: the superseded store's opens must NOT reach the instrument, so a slot still pointing at oldStore can no longer satisfy the assertion by coincidence when both stores hold equal counts. Fail-before (temporary probe, not committed): with a foreign provider returning 7 registered before the service starts, the OLD body fails `Assert.Equal(0, baseline)` with `Expected: 0 / Actual: 7`; the NEW delta body passes the identical scenario. Honest residual documented on the helper: a gauge has one slot and last-writer-wins, so a registrant that clobbers it BETWEEN the two reads is unrecoverable read-side — that needs row-32's mutual exclusion. It cannot arise today (this is the only file in the assembly constructing a BundleSessionEvictionService, and test assemblies are separate processes); a second such file should bring the collection treatment with it. Transport.Tests: 180/180 green. --- .../Import/BundleSessionObservabilityTests.cs | 93 +++++++++++++++---- 1 file changed, 77 insertions(+), 16 deletions(-) diff --git a/tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Import/BundleSessionObservabilityTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Import/BundleSessionObservabilityTests.cs index ea6d17e2..1c85e604 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Import/BundleSessionObservabilityTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Import/BundleSessionObservabilityTests.cs @@ -215,30 +215,39 @@ public sealed class BundleSessionObservabilityTests public async Task EvictionService_RegistersTheGaugeProvider_AndClearsItOnStop() { // The eviction service is Transport's only lifecycle seam, so it owns the - // process-global gauge slot. The gauge is read as a delta against a baseline - // because the instrument is a process-wide static. + // process-global gauge slot. Every assertion here is a DELTA over a scoped action + // against this test's own store (register row 74) — see ReadGaugeDelta. var clock = new TestClock(DateTimeOffset.UtcNow); var store = new BundleSessionStore(Options.Create(new TransportOptions()), clock, NullLogger.Instance); var service = new BundleSessionEvictionService(store, NullLogger.Instance); - var baseline = ReadGauge(); - await service.StartAsync(CancellationToken.None); try { - store.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30))); - store.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30))); + // While registered, this store's opens are what the gauge reports. + var whileRegistered = ReadGaugeDelta(() => + { + store.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30))); + store.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30))); + }); - Assert.Equal(2, ReadGauge()); + Assert.Equal(2, whileRegistered); } finally { await service.StopAsync(CancellationToken.None); } - // After the clear the gauge falls back to reporting 0, not through a dead store. - Assert.Equal(0, ReadGauge()); - Assert.Equal(0, baseline); + // After the clear the gauge no longer reads through this store: opening more + // sessions on it moves the instrument by nothing. Stated as a delta this is a + // STRONGER claim than the old `Assert.Equal(0, ReadGauge())` — that one merely + // said "the instrument reports zero", which any unregistered slot satisfies, + // and which a foreign registrant would have failed for reasons unrelated to + // the clear. + var afterClear = ReadGaugeDelta(() => + store.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30)))); + + Assert.Equal(0, afterClear); } [Fact] @@ -257,29 +266,81 @@ public sealed class BundleSessionObservabilityTests await newService.StartAsync(CancellationToken.None); try { - newStore.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30))); - - // Late stop of the superseded instance. + // Late stop of the superseded instance, then prove the NEWER store is still + // the one driving the gauge — by moving it and measuring the movement. await oldService.StopAsync(CancellationToken.None); - Assert.Equal(1, ReadGauge()); + var newStoreStillDrivesIt = ReadGaugeDelta(() => + newStore.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30)))); + + Assert.Equal(1, newStoreStillDrivesIt); + + // Negative control on the same window: the superseded store is detached, so + // its opens must not reach the instrument. Without this, a provider slot left + // pointing at oldStore would still satisfy the assertion above by coincidence + // whenever both stores happened to hold equal counts. + var oldStoreIsDetached = ReadGaugeDelta(() => + oldStore.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30)))); + + Assert.Equal(0, oldStoreIsDetached); } finally { await newService.StopAsync(CancellationToken.None); } - Assert.Equal(0, ReadGauge()); + var afterClear = ReadGaugeDelta(() => + newStore.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30)))); + + Assert.Equal(0, afterClear); } // ------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------ + /// + /// Runs between two observations of the open-import-sessions + /// gauge and returns how far the instrument MOVED across it. + /// + /// + /// + /// Register row 74. These tests used to assert absolute gauge values — + /// Assert.Equal(0, baseline), Assert.Equal(2, ReadGauge()) — against + /// what is a single process-global provider slot + /// (SetImportSessionCountProvider / ClearImportSessionCountProvider). + /// That is the same shape as the register row 32 flake in + /// QueueDepthGaugeTests: correct only for as long as this file is the slot's + /// sole registrant, and silently wrong the day a sibling suite writes it. A delta + /// carries no assumption about what the slot held on entry, so a non-zero inherited + /// baseline cancels out. + /// + /// + /// Honest residual, and the reason this is a delta rather than a full fix: unlike + /// — where every measurement in the window is + /// driven synchronously by this test's own Open call, making it genuinely + /// immune — a gauge has ONE slot and last-writer-wins. If a concurrent registrant + /// overwrote the slot BETWEEN the two reads, no read-side technique could recover; + /// that case needs mutual exclusion on the slot (row 32's serialized collection). + /// It cannot arise today: this is the only file in the assembly that constructs a + /// BundleSessionEvictionService, and test assemblies run in separate + /// processes. A second such file should bring the collection treatment with it. + /// + /// + /// The action to measure across. + /// The gauge's movement while ran. + private static long ReadGaugeDelta(Action act) + { + var before = ReadGauge(); + act(); + return ReadGauge() - before; + } + /// /// Forces one synchronous observation of the open-import-sessions gauge and returns /// the measured value. Mirrors the MeterListener helper used by the Communication - /// and StoreAndForward telemetry suites. + /// and StoreAndForward telemetry suites. Private to by + /// convention — see its remarks for why no test reads this absolutely. /// /// The gauge's current value, or 0 if it never reported. private static long ReadGauge()