test(dcl+communication+storeandforward+kpihistory+host): close register row-40-class sleeps + row-32 provider leak (arch-review 2026-08-19 P8-8 + P8-9)
P8-8: replace the remaining wall-clock sleep-then-assert instances outside Communication.Tests' Medium-wave sweep with the same row-40 house pattern — continuous polling (AssertHoldsFor) for ABSENCE assertions instead of a single end-of-window sample, which cannot distinguish "stayed quiet throughout" from "hadn't happened yet when the one sample landed" and cannot catch a value that flips wrong and back before that sample. - DataConnectionActorBatchTests: QualityCounters_ArePushedOnTransitionOnly_AndCoalesced (:206/:214 class) converted to AssertHoldsFor; ResolutionProbes_BackOff... (:181/:184, a genuine real-time backoff-RATE assertion, not a sync-primitive stand-in) converted to AssertRoundsWithinWindow, which enforces the ceiling continuously (fails fast on a no-backoff regression) while the floor still necessarily waits out the window. - SiteAlarmLiveCacheServiceTests (:452-454, :470-472): converted to the project's own AssertHoldsFor (mirrors SiteAlarmAggregatorActorTests', same assembly). - StoreAndForwardServiceTests (:891-892): async AssertHoldsForAsync variant (the class runs on the async test thread). - KpiHistoryRecorderActorTests (:1043-1049): two skip-path FoldCount==0 absence checks converted. Each helper is a private per-file copy, matching the Medium-wave T17 precedent (SiteAlarmAggregatorActorTests.AssertHoldsFor) rather than a shared TestSupport utility. Proof of the mechanism: a standalone (non-repo) harness run 100 trials of a real background thread flipping a boolean true then back to false inside a 300ms window — Thread.Sleep(300)+one sample missed the violation 100/100 times (false pass), continuous 20ms polling caught it 100/100 times. Most of the actual conversions here guard monotonic counters (their single-sample variant would eventually see the final value too), so each converted test carries an inline honesty note on what specifically the conversion buys for that assertion (fail-fast vs. true flip-back detection). P8-9: ScadaBridgeTelemetryTests.EmitHelpers_DoNotThrow called SetQueueDepthProvider(() => 5) against the process-global row-32 provider slot with no clear and no [Collection] — a landmine for any future same-process reader of scadabridge.store_and_forward.queue.depth. Fixed with compare-and-clear in a finally via the existing (production, untouched) ClearQueueDepthProvider. Searched this assembly and the whole test tree for a second same-process reader that asserts on the value: none exists today (MetricsEndpointTests scrapes /metrics but only asserts on the response shape, and the only value-asserting test, QueueDepthGaugeTests, lives in StoreAndForward.Tests — a separate test-host process the leak cannot reach), so no [Collection] was added; the test's doc comment records the search and flags it for re-check if that changes. Test-only change, zero production edits. Full suites green: DCL.Tests 342/342, Communication.Tests 729/729, StoreAndForward.Tests 135/135, KpiHistory.Tests 54/54, Host.Tests 522/522. Each converted test additionally looped 20x in isolation with zero flakes.
This commit is contained in:
@@ -15,9 +15,37 @@ public class ScadaBridgeTelemetryTests
|
||||
Assert.Equal("ZB.MOM.WW.ScadaBridge", ScadaBridgeTelemetry.MeterName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register row 32 class: <c>SetQueueDepthProvider</c> writes the SAME process-global
|
||||
/// slot <c>QueueDepthGaugeCollection</c> (StoreAndForward.Tests) exists to serialize
|
||||
/// access to — this test was mutating it with no clear and no <c>[Collection]</c>,
|
||||
/// leaving a stray <c>() => 5</c> provider registered in the process for the rest of
|
||||
/// this assembly's run. Since the meter/instrument state is a single static field on
|
||||
/// <c>ScadaBridgeTelemetry</c> (Commons), the leak is scoped to whatever process this
|
||||
/// assembly's test host runs in — it cannot reach StoreAndForward.Tests (a separate
|
||||
/// assembly with its own test-host process), but it IS visible within Host.Tests, whose
|
||||
/// <c>MetricsEndpointTests</c> boots a real in-process host (<c>WebApplicationFactory</c>
|
||||
/// shares this AppDomain, not a child process) and scrapes <c>/metrics</c>, which
|
||||
/// enumerates every registered gauge including this one.
|
||||
///
|
||||
/// Honesty note on what was and wasn't proven: a search of this assembly (and the whole
|
||||
/// test tree) found no test today that asserts on the VALUE of
|
||||
/// <c>scadabridge.store_and_forward.queue.depth</c> from Host.Tests or any other
|
||||
/// same-process reader — <c>MetricsEndpointTests</c> only asserts the endpoint returns
|
||||
/// Prometheus-shaped text, not this series' content, and the only test that DOES assert
|
||||
/// on the value (<c>QueueDepthGaugeTests</c>) lives in StoreAndForward.Tests, a distinct
|
||||
/// test-host process the leak cannot reach. So there is no currently-reproducible flake
|
||||
/// to demonstrate here (no fork of this test with the leak left in has an assertion to
|
||||
/// break) — the defect is a landmine for a FUTURE same-process reader, not a live one.
|
||||
/// The fix (compare-and-clear in <c>finally</c>) closes the landmine regardless; a
|
||||
/// <c>[Collection]</c> is intentionally not added since no second same-process reader of
|
||||
/// this slot exists to serialize against as of this writing — re-run this search before
|
||||
/// removing the guard below if that changes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EmitHelpers_DoNotThrow()
|
||||
{
|
||||
Func<long> queueDepthProvider = () => 5;
|
||||
var ex = Record.Exception(() =>
|
||||
{
|
||||
ScadaBridgeTelemetry.RecordDeploymentApplied();
|
||||
@@ -27,9 +55,20 @@ public class ScadaBridgeTelemetryTests
|
||||
ScadaBridgeTelemetry.LiveAlarmAggregatorStarted();
|
||||
ScadaBridgeTelemetry.LiveAlarmAggregatorStopped();
|
||||
ScadaBridgeTelemetry.RecordLiveAlarmStreamReconnect();
|
||||
ScadaBridgeTelemetry.SetQueueDepthProvider(() => 5);
|
||||
ScadaBridgeTelemetry.SetQueueDepthProvider(queueDepthProvider);
|
||||
});
|
||||
|
||||
Assert.Null(ex);
|
||||
try
|
||||
{
|
||||
Assert.Null(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Compare-and-clear: only removes OUR delegate from the process-global slot, so
|
||||
// a legitimate concurrent registrant (a real StoreAndForwardService started
|
||||
// elsewhere in this process) is never stomped — mirrors
|
||||
// ScadaBridgeTelemetry.ClearQueueDepthProvider's own doc'd contract.
|
||||
ScadaBridgeTelemetry.ClearQueueDepthProvider(queueDepthProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user