428b8db024
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.
75 lines
3.8 KiB
C#
75 lines
3.8 KiB
C#
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
|
|
|
|
/// <summary>
|
|
/// Infrastructure-free guards for <see cref="ScadaBridgeTelemetry"/>: the meter name is the
|
|
/// stable value registered with OTel, and the emit helpers are safe to call (they are no-op
|
|
/// until follow-on tasks wire real emit points and a listener attaches).
|
|
/// </summary>
|
|
public class ScadaBridgeTelemetryTests
|
|
{
|
|
[Fact]
|
|
public void MeterName_IsStableValue()
|
|
{
|
|
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();
|
|
ScadaBridgeTelemetry.RecordInboundApiRequest("X");
|
|
ScadaBridgeTelemetry.SiteStreamSubscriberOpened();
|
|
ScadaBridgeTelemetry.SiteStreamSubscriberClosed();
|
|
ScadaBridgeTelemetry.LiveAlarmAggregatorStarted();
|
|
ScadaBridgeTelemetry.LiveAlarmAggregatorStopped();
|
|
ScadaBridgeTelemetry.RecordLiveAlarmStreamReconnect();
|
|
ScadaBridgeTelemetry.SetQueueDepthProvider(queueDepthProvider);
|
|
});
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|