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:
@@ -497,6 +497,28 @@ public class KpiHistoryRecorderActorTests : TestKit
|
||||
NullLogger<KpiHistoryRecorderActor>.Instance)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Row-40 house pattern for an ABSENCE assertion (mirrors the Medium-wave
|
||||
/// <c>SiteAlarmAggregatorActorTests.AssertHoldsFor</c> treatment): proves
|
||||
/// <paramref name="holds"/> keeps being true for the whole <paramref name="window"/> by
|
||||
/// polling, rather than a single blind <c>Thread.Sleep(window)</c> followed by one
|
||||
/// end-of-window sample. A bare sleep-then-assert cannot distinguish "the mechanism
|
||||
/// correctly stayed quiet" from "the mechanism just hadn't run yet when the one sample
|
||||
/// was taken", and it can't catch a value that flips wrong and then flips back before
|
||||
/// that single sample. This fails immediately the first time <paramref name="holds"/>
|
||||
/// goes false, and only returns once the full window has elapsed with it still true.
|
||||
/// </summary>
|
||||
private static void AssertHoldsFor(TimeSpan window, Func<bool> holds, string because)
|
||||
{
|
||||
var deadline = DateTime.UtcNow + window;
|
||||
while (true)
|
||||
{
|
||||
Assert.True(holds(), because);
|
||||
if (DateTime.UtcNow >= deadline) return;
|
||||
Thread.Sleep(20);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SampleTick_WritesHealthySourceSamples_AndThrowingSourceDoesNotAbortTick()
|
||||
{
|
||||
@@ -1040,13 +1062,13 @@ public class KpiHistoryRecorderActorTests : TestKit
|
||||
interval: TimeSpan.FromMilliseconds(50));
|
||||
|
||||
// …and then skip: no slices are ever folded.
|
||||
Thread.Sleep(300);
|
||||
Assert.Equal(0, repository.FoldCount);
|
||||
AssertHoldsFor(TimeSpan.FromMilliseconds(300), () => repository.FoldCount == 0,
|
||||
"watermark inside the lookback tail must skip the backfill entirely");
|
||||
|
||||
// A second BackfillTick is a no-op (the done-latch is set).
|
||||
actor.Tell(KpiHistoryRecorderActor.BackfillTick.Instance);
|
||||
Thread.Sleep(300);
|
||||
Assert.Equal(0, repository.FoldCount);
|
||||
AssertHoldsFor(TimeSpan.FromMilliseconds(300), () => repository.FoldCount == 0,
|
||||
"the done-latch must make a second BackfillTick a no-op");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user