docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
This commit is contained in:
+3
-2
@@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// M2.16 (#30) idempotency regression — code-review finding on commit d81f747.
|
||||
/// Idempotency regression — code-review finding on commit d81f747.
|
||||
/// <para>
|
||||
/// <see cref="ServiceCollectionExtensions.AddSiteEventLogHealthMetricsBridge"/> uses a
|
||||
/// factory-lambda overload of <c>AddHostedService</c>, which sets only
|
||||
@@ -18,10 +18,11 @@ namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests;
|
||||
/// </summary>
|
||||
public class AddSiteEventLogHealthMetricsBridgeTests
|
||||
{
|
||||
/// <summary>Verifies that adding the site event log health metrics bridge twice registers the hosted service only once.</summary>
|
||||
[Fact]
|
||||
public void AddSiteEventLogHealthMetricsBridge_IsIdempotent_DoesNotDoubleRegister_HostedService()
|
||||
{
|
||||
// M2.16 (#30): calling the bridge method twice must register exactly one
|
||||
// Calling the bridge method twice must register exactly one
|
||||
// SiteEventLogFailureCountReporter. Without the marker-type guard the
|
||||
// ImplementationType == check was a no-op for factory-lambda registrations,
|
||||
// so the second call would have added a second hosted service (two timers).
|
||||
|
||||
+3
-1
@@ -1,7 +1,7 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Bundle C (M5-T7) regression coverage. The canonical audit redactor
|
||||
/// Regression coverage: the canonical audit redactor
|
||||
/// (<see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Redaction.ScadaBridgeAuditRedactor"/>) increments
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Payload.IAuditRedactionFailureCounter"/> every time
|
||||
/// a header/body/SQL-param redactor stage throws and the redactor has to
|
||||
@@ -17,6 +17,7 @@ public class AuditRedactionFailureMetricTests
|
||||
{
|
||||
private readonly SiteHealthCollector _collector = new();
|
||||
|
||||
/// <summary>Verifies that incrementing the counter three times makes it report three.</summary>
|
||||
[Fact]
|
||||
public void Increment_Three_Times_Counter_Reports_3()
|
||||
{
|
||||
@@ -29,6 +30,7 @@ public class AuditRedactionFailureMetricTests
|
||||
Assert.Equal(3, report.AuditRedactionFailure);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the report payload includes the audit redaction failure count as zero by default.</summary>
|
||||
[Fact]
|
||||
public void Report_Payload_Includes_AuditRedactionFailure_AsZeroByDefault()
|
||||
{
|
||||
|
||||
+31
-10
@@ -12,13 +12,18 @@ internal sealed class TestTimeProvider : TimeProvider
|
||||
{
|
||||
private DateTimeOffset _utcNow;
|
||||
|
||||
/// <summary>Initializes the provider at the given start time.</summary>
|
||||
/// <param name="startTime">The initial UTC time reported by the provider.</param>
|
||||
public TestTimeProvider(DateTimeOffset startTime)
|
||||
{
|
||||
_utcNow = startTime;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override DateTimeOffset GetUtcNow() => _utcNow;
|
||||
|
||||
/// <summary>Advances the fake clock by the given duration.</summary>
|
||||
/// <param name="duration">The amount of time to advance.</param>
|
||||
public void Advance(TimeSpan duration) => _utcNow += duration;
|
||||
}
|
||||
|
||||
@@ -27,6 +32,7 @@ public class CentralHealthAggregatorTests
|
||||
private readonly TestTimeProvider _timeProvider;
|
||||
private readonly CentralHealthAggregator _aggregator;
|
||||
|
||||
/// <summary>Initializes the tests with a controllable clock and a fresh aggregator.</summary>
|
||||
public CentralHealthAggregatorTests()
|
||||
{
|
||||
_timeProvider = new TestTimeProvider(DateTimeOffset.UtcNow);
|
||||
@@ -72,6 +78,7 @@ public class CentralHealthAggregatorTests
|
||||
EnabledInstanceCount: 0,
|
||||
DisabledInstanceCount: 0);
|
||||
|
||||
/// <summary>Verifies that processing a report stores the state for a new site.</summary>
|
||||
[Fact]
|
||||
public void ProcessReport_StoresState_ForNewSite()
|
||||
{
|
||||
@@ -83,6 +90,7 @@ public class CentralHealthAggregatorTests
|
||||
Assert.Equal(1, state.LastSequenceNumber);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that processing a report updates the state when the sequence number increases.</summary>
|
||||
[Fact]
|
||||
public void ProcessReport_UpdatesState_WhenSequenceIncreases()
|
||||
{
|
||||
@@ -93,6 +101,7 @@ public class CentralHealthAggregatorTests
|
||||
Assert.Equal(2, state!.LastSequenceNumber);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a report whose sequence number is not greater than the stored one is rejected as stale.</summary>
|
||||
[Fact]
|
||||
public void ProcessReport_RejectsStaleReport_WhenSequenceNotGreater()
|
||||
{
|
||||
@@ -103,6 +112,7 @@ public class CentralHealthAggregatorTests
|
||||
Assert.Equal(5, state!.LastSequenceNumber);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a report with a sequence number equal to the stored one is rejected.</summary>
|
||||
[Fact]
|
||||
public void ProcessReport_RejectsEqualSequence()
|
||||
{
|
||||
@@ -113,6 +123,7 @@ public class CentralHealthAggregatorTests
|
||||
Assert.Equal(5, state!.LastSequenceNumber);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a site goes offline when no report arrives within the timeout.</summary>
|
||||
[Fact]
|
||||
public void OfflineDetection_SiteGoesOffline_WhenNoReportWithinTimeout()
|
||||
{
|
||||
@@ -126,6 +137,7 @@ public class CentralHealthAggregatorTests
|
||||
Assert.False(_aggregator.GetSiteState("site-1")!.IsOnline);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an offline site comes back online when a report is received.</summary>
|
||||
[Fact]
|
||||
public void OnlineRecovery_SiteComesBackOnline_WhenReportReceived()
|
||||
{
|
||||
@@ -141,6 +153,7 @@ public class CentralHealthAggregatorTests
|
||||
Assert.True(_aggregator.GetSiteState("site-1")!.IsOnline);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a site remains online when a report arrives within the timeout.</summary>
|
||||
[Fact]
|
||||
public void OfflineDetection_SiteRemainsOnline_WhenReportWithinTimeout()
|
||||
{
|
||||
@@ -152,6 +165,7 @@ public class CentralHealthAggregatorTests
|
||||
Assert.True(_aggregator.GetSiteState("site-1")!.IsOnline);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that all known sites are returned by the all-states query.</summary>
|
||||
[Fact]
|
||||
public void GetAllSiteStates_ReturnsAllKnownSites()
|
||||
{
|
||||
@@ -164,6 +178,7 @@ public class CentralHealthAggregatorTests
|
||||
Assert.Contains("site-2", states.Keys);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the state lookup returns null for an unknown site.</summary>
|
||||
[Fact]
|
||||
public void GetSiteState_ReturnsNull_ForUnknownSite()
|
||||
{
|
||||
@@ -171,6 +186,7 @@ public class CentralHealthAggregatorTests
|
||||
Assert.Null(state);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that processing a report stores the latest report for the site.</summary>
|
||||
[Fact]
|
||||
public void ProcessReport_StoresLatestReport()
|
||||
{
|
||||
@@ -182,13 +198,14 @@ public class CentralHealthAggregatorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HealthMonitoring-002 regression: SiteHealthState is mutated from multiple
|
||||
/// Regression test: SiteHealthState is mutated from multiple
|
||||
/// threads (ProcessReport, MarkHeartbeat, CheckForOfflineSites). With a mutable
|
||||
/// class and unsynchronized field writes, a snapshot read could observe a torn
|
||||
/// or half-applied state. The state must be immutable and every transition an
|
||||
/// atomic reference swap, so a snapshot is always internally consistent and the
|
||||
/// monotonic sequence-number guard is never subverted by a lost update.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ProcessReport_ConcurrentUpdates_NeverLoseSequenceOrTearState()
|
||||
{
|
||||
@@ -237,7 +254,7 @@ public class CentralHealthAggregatorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HealthMonitoring-007 regression: a heartbeat for a site that has not yet
|
||||
/// Regression test: a heartbeat for a site that has not yet
|
||||
/// sent a full report (e.g. immediately after a central restart/failover, when
|
||||
/// the aggregator's in-memory state is empty) must register the site as online
|
||||
/// rather than being silently discarded. Otherwise reachable sites show as
|
||||
@@ -258,9 +275,9 @@ public class CentralHealthAggregatorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression test for HealthMonitoring-015. A heartbeat-only registered site
|
||||
/// Regression test: a heartbeat-only registered site
|
||||
/// has never processed a full report, so <see cref="SiteHealthState.LastReportReceivedAt"/>
|
||||
/// must be <c>null</c> — not the <c>DateTimeOffset.MinValue</c> (year-0001)
|
||||
/// must be <c>null</c> — not the <c>DateTimeOffset.MinValue</c> (year 0001)
|
||||
/// sentinel that the UI would otherwise render as a ~2000-year-stale timestamp.
|
||||
/// The "no report yet" signal must be an explicit nullable state, consistent
|
||||
/// with <see cref="SiteHealthState.LatestReport"/>.
|
||||
@@ -276,7 +293,7 @@ public class CentralHealthAggregatorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression test for HealthMonitoring-015. Once a full report is processed
|
||||
/// Regression test: once a full report is processed
|
||||
/// for a heartbeat-registered site, <see cref="SiteHealthState.LastReportReceivedAt"/>
|
||||
/// becomes a real (non-null) instant.
|
||||
/// </summary>
|
||||
@@ -294,6 +311,7 @@ public class CentralHealthAggregatorTests
|
||||
Assert.Equal(reportTime, state.LastReportReceivedAt);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that heartbeats keep a site online between full reports.</summary>
|
||||
[Fact]
|
||||
public void MarkHeartbeat_KeepsSiteOnline_BetweenReports()
|
||||
{
|
||||
@@ -310,6 +328,7 @@ public class CentralHealthAggregatorTests
|
||||
Assert.True(_aggregator.GetSiteState("site-1")!.IsOnline);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a heartbeat brings an offline site back online.</summary>
|
||||
[Fact]
|
||||
public void MarkHeartbeat_BringsOfflineSiteBackOnline()
|
||||
{
|
||||
@@ -324,7 +343,7 @@ public class CentralHealthAggregatorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HealthMonitoring-020 regression: an offline-to-online transition must
|
||||
/// Regression test: an offline-to-online transition must
|
||||
/// be backed by a fresh LastHeartbeatAt. Previously MarkHeartbeat used
|
||||
/// <c>max(receivedAt, existing.LastHeartbeatAt)</c>, so an out-of-order
|
||||
/// heartbeat carrying an older timestamp would bring the site online with
|
||||
@@ -357,7 +376,7 @@ public class CentralHealthAggregatorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HealthMonitoring-005 regression: the synthetic "central" site has no
|
||||
/// Regression test: the synthetic "central" site has no
|
||||
/// heartbeat source — its LastHeartbeatAt is only bumped by the 30s
|
||||
/// CentralHealthReportLoop self-report. A single skipped/late self-report
|
||||
/// (leader GC pause, brief stall, mid-failover) would leave it with no signal
|
||||
@@ -382,6 +401,7 @@ public class CentralHealthAggregatorTests
|
||||
"central must survive a single missed self-report");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the synthetic central site still goes offline on a genuine loss of self-reports.</summary>
|
||||
[Fact]
|
||||
public void OfflineDetection_CentralSite_StillGoesOfflineOnGenuineLoss()
|
||||
{
|
||||
@@ -395,7 +415,7 @@ public class CentralHealthAggregatorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HealthMonitoring-013 regression: the offline-check cadence must be derived
|
||||
/// Regression test: the offline-check cadence must be derived
|
||||
/// from the *shorter* of <see cref="HealthMonitoringOptions.OfflineTimeout"/>
|
||||
/// and <see cref="HealthMonitoringOptions.CentralOfflineTimeout"/>, so that if
|
||||
/// an operator configures <c>CentralOfflineTimeout</c> smaller than
|
||||
@@ -424,6 +444,7 @@ public class CentralHealthAggregatorTests
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a sequence number reset is rejected until it exceeds the previous maximum.</summary>
|
||||
[Fact]
|
||||
public void SequenceNumberReset_RejectedUntilExceedsPrevMax()
|
||||
{
|
||||
@@ -495,7 +516,7 @@ public class CentralHealthAggregatorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review 01 underdeveloped #7: "when did the site drop" was unanswerable.
|
||||
/// Previously "when did the site drop" was unanswerable.
|
||||
/// Every online↔offline flip stamps <see cref="SiteHealthState.LastStatusChangeAt"/>.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
@@ -515,7 +536,7 @@ public class CentralHealthAggregatorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Round-2 N3: a site known only via heartbeats (LastReportReceivedAt == null)
|
||||
/// A site known only via heartbeats (LastReportReceivedAt == null)
|
||||
/// previously skipped the staleness check forever — a report pipeline dead
|
||||
/// from FIRST BOOT showed "online with no metrics" indefinitely. FirstSeenAt
|
||||
/// anchors the window instead (LastHeartbeatAt cannot: heartbeats advance it).
|
||||
|
||||
@@ -5,7 +5,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.Health;
|
||||
namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// HealthMonitoring-009 regression: the central self-report loop had no test
|
||||
/// Regression tests for the central self-report loop, which had no test
|
||||
/// coverage at all. These tests exercise leader-only gating (SelfIsPrimary),
|
||||
/// self-report generation for siteId="central", and monotonic sequence
|
||||
/// assignment.
|
||||
@@ -14,24 +14,33 @@ public class CentralHealthReportLoopTests
|
||||
{
|
||||
private sealed class FakeClusterNodeProvider : IClusterNodeProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool SelfIsPrimary { get; set; }
|
||||
/// <summary>Gets or sets the nodes returned by <c>GetClusterNodes</c>.</summary>
|
||||
public IReadOnlyList<NodeStatus> Nodes { get; set; } = [];
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<NodeStatus> GetClusterNodes() => Nodes;
|
||||
}
|
||||
|
||||
private sealed class RecordingAggregator : ICentralHealthAggregator
|
||||
{
|
||||
/// <summary>Gets the reports passed to <see cref="ProcessReport"/>.</summary>
|
||||
public List<SiteHealthReport> Processed { get; } = [];
|
||||
/// <inheritdoc />
|
||||
public void ProcessReport(SiteHealthReport report) => Processed.Add(report);
|
||||
/// <inheritdoc />
|
||||
public void MarkHeartbeat(string siteId, DateTimeOffset receivedAt) { }
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyDictionary<string, SiteHealthState> GetAllSiteStates() =>
|
||||
new Dictionary<string, SiteHealthState>();
|
||||
/// <inheritdoc />
|
||||
public SiteHealthState? GetSiteState(string siteId) => null;
|
||||
/// <inheritdoc />
|
||||
public void PruneUnknownSites(IReadOnlyCollection<string> knownSiteIds) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HealthMonitoring-022 de-flake: <see cref="CentralHealthReportLoop"/>'s
|
||||
/// De-flake note: <see cref="CentralHealthReportLoop"/>'s
|
||||
/// internal cadence is a real <see cref="PeriodicTimer"/>, so the loop is
|
||||
/// timing-sensitive. We can't drive a virtual clock (PeriodicTimer doesn't
|
||||
/// consume <see cref="TimeProvider"/>) without refactoring the production
|
||||
@@ -82,6 +91,8 @@ public class CentralHealthReportLoopTests
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the loop generates central reports when this node is the primary.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GeneratesCentralReports_WhenSelfIsPrimary()
|
||||
{
|
||||
@@ -97,7 +108,7 @@ public class CentralHealthReportLoopTests
|
||||
collector, aggregator, clusterNodes, options,
|
||||
NullLogger<CentralHealthReportLoop>.Instance);
|
||||
|
||||
// HealthMonitoring-022: wait up to 5 s for at least one report to fire
|
||||
// Wait up to 5 s for at least one report to fire
|
||||
// rather than fixed-budget Task.Delay; tolerates slow CI runners.
|
||||
await RunLoopUntil(loop, () => aggregator.Processed.Count >= 1);
|
||||
|
||||
@@ -106,6 +117,8 @@ public class CentralHealthReportLoopTests
|
||||
r => Assert.Equal(CentralHealthReportLoop.CentralSiteId, r.SiteId));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the loop generates no reports when this node is not the primary.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GeneratesNoReports_WhenNotPrimary()
|
||||
{
|
||||
@@ -126,6 +139,8 @@ public class CentralHealthReportLoopTests
|
||||
Assert.Empty(aggregator.Processed);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the loop assigns monotonically increasing sequence numbers.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AssignsMonotonicSequenceNumbers()
|
||||
{
|
||||
@@ -141,7 +156,7 @@ public class CentralHealthReportLoopTests
|
||||
collector, aggregator, clusterNodes, options,
|
||||
NullLogger<CentralHealthReportLoop>.Instance);
|
||||
|
||||
// HealthMonitoring-022: wait up to 5 s for at least 2 reports rather
|
||||
// Wait up to 5 s for at least 2 reports rather
|
||||
// than a fixed 300 ms window that could miss the second tick on a
|
||||
// slow CI runner; the assertion below proves the sequence is monotonic.
|
||||
await RunLoopUntil(loop, () => aggregator.Processed.Count >= 2);
|
||||
@@ -157,7 +172,7 @@ public class CentralHealthReportLoopTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HealthMonitoring-006 regression: the central loop's sequence-number seed
|
||||
/// Regression: the central loop's sequence-number seed
|
||||
/// must be derived from the injected <see cref="TimeProvider"/> (Unix-ms),
|
||||
/// not from <c>DateTimeOffset.UtcNow</c> read at field initialization, so the
|
||||
/// seeding strategy is deterministically testable.
|
||||
@@ -180,13 +195,14 @@ public class CentralHealthReportLoopTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HealthMonitoring-018 regression: when <see cref="ICentralHealthAggregator.ProcessReport"/>
|
||||
/// Regression: when <see cref="ICentralHealthAggregator.ProcessReport"/>
|
||||
/// throws, the per-interval counters that
|
||||
/// <see cref="SiteHealthCollector.CollectReport"/> just drained must be
|
||||
/// restored back into the shared collector so they roll forward into the
|
||||
/// next interval rather than being silently lost. Same shape as the
|
||||
/// HealthMonitoring-017 fix in <see cref="HealthReportSender"/>.
|
||||
/// restore-on-failure fix in <see cref="HealthReportSender"/>.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ProcessReportFailure_PreservesIntervalCountersForNextReport()
|
||||
{
|
||||
@@ -216,7 +232,7 @@ public class CentralHealthReportLoopTests
|
||||
collector, aggregator, clusterNodes, options,
|
||||
NullLogger<CentralHealthReportLoop>.Instance);
|
||||
|
||||
// HealthMonitoring-022: the first ProcessReport call throws (counters
|
||||
// The first ProcessReport call throws (counters
|
||||
// get restored), the second succeeds. Wait up to 5 s for that second
|
||||
// (successful) call rather than a fixed 450 ms budget.
|
||||
await RunLoopUntil(loop, () => aggregator.Processed.Count >= 1);
|
||||
@@ -236,13 +252,15 @@ public class CentralHealthReportLoopTests
|
||||
/// <see cref="ICentralHealthAggregator"/> whose first <c>ProcessReport</c>
|
||||
/// call throws (only the first), then subsequent calls succeed. Used by
|
||||
/// <see cref="ProcessReportFailure_PreservesIntervalCountersForNextReport"/>
|
||||
/// to verify the HealthMonitoring-018 restore-on-failure path.
|
||||
/// to verify the restore-on-failure path.
|
||||
/// </summary>
|
||||
private sealed class FailingThenSucceedingAggregator : ICentralHealthAggregator
|
||||
{
|
||||
private int _callCount;
|
||||
/// <summary>Gets the reports that were processed successfully.</summary>
|
||||
public List<SiteHealthReport> Processed { get; } = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ProcessReport(SiteHealthReport report)
|
||||
{
|
||||
var n = Interlocked.Increment(ref _callCount);
|
||||
@@ -253,13 +271,19 @@ public class CentralHealthReportLoopTests
|
||||
Processed.Add(report);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void MarkHeartbeat(string siteId, DateTimeOffset receivedAt) { }
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyDictionary<string, SiteHealthState> GetAllSiteStates() =>
|
||||
new Dictionary<string, SiteHealthState>();
|
||||
/// <inheritdoc />
|
||||
public SiteHealthState? GetSiteState(string siteId) => null;
|
||||
/// <inheritdoc />
|
||||
public void PruneUnknownSites(IReadOnlyCollection<string> knownSiteIds) { }
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the active-node flag is set on the report even when this node is not the primary.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SetsActiveNodeFlag_EvenWhenNotPrimary()
|
||||
{
|
||||
|
||||
+14
-1
@@ -3,7 +3,7 @@ using Microsoft.Extensions.Options;
|
||||
namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// HealthMonitoring-014 regression: <see cref="HealthMonitoringOptions"/> intervals
|
||||
/// Regression tests: <see cref="HealthMonitoringOptions"/> intervals
|
||||
/// are fed straight into <c>new PeriodicTimer(...)</c>, which throws
|
||||
/// <see cref="ArgumentOutOfRangeException"/> for a zero/negative period. A
|
||||
/// misconfigured <c>appsettings.json</c> must be rejected by an
|
||||
@@ -15,6 +15,7 @@ public class HealthMonitoringOptionsValidatorTests
|
||||
private static ValidateOptionsResult Validate(HealthMonitoringOptions options) =>
|
||||
new HealthMonitoringOptionsValidator().Validate(Options.DefaultName, options);
|
||||
|
||||
/// <summary>Verifies that the default options pass validation.</summary>
|
||||
[Fact]
|
||||
public void DefaultOptions_AreValid()
|
||||
{
|
||||
@@ -22,6 +23,7 @@ public class HealthMonitoringOptionsValidatorTests
|
||||
Assert.True(result.Succeeded, result.FailureMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a zero report interval is rejected.</summary>
|
||||
[Fact]
|
||||
public void ZeroReportInterval_IsRejected()
|
||||
{
|
||||
@@ -31,6 +33,7 @@ public class HealthMonitoringOptionsValidatorTests
|
||||
Assert.Contains("ReportInterval", result.FailureMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a negative report interval is rejected.</summary>
|
||||
[Fact]
|
||||
public void NegativeReportInterval_IsRejected()
|
||||
{
|
||||
@@ -40,6 +43,7 @@ public class HealthMonitoringOptionsValidatorTests
|
||||
Assert.Contains("ReportInterval", result.FailureMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a zero offline timeout is rejected.</summary>
|
||||
[Fact]
|
||||
public void ZeroOfflineTimeout_IsRejected()
|
||||
{
|
||||
@@ -49,6 +53,7 @@ public class HealthMonitoringOptionsValidatorTests
|
||||
Assert.Contains("OfflineTimeout", result.FailureMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a zero central offline timeout is rejected.</summary>
|
||||
[Fact]
|
||||
public void ZeroCentralOfflineTimeout_IsRejected()
|
||||
{
|
||||
@@ -58,6 +63,7 @@ public class HealthMonitoringOptionsValidatorTests
|
||||
Assert.Contains("CentralOfflineTimeout", result.FailureMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a central offline timeout shorter than the offline timeout is rejected.</summary>
|
||||
[Fact]
|
||||
public void CentralOfflineTimeout_ShorterThanOfflineTimeout_IsRejected()
|
||||
{
|
||||
@@ -71,6 +77,7 @@ public class HealthMonitoringOptionsValidatorTests
|
||||
Assert.Contains("CentralOfflineTimeout", result.FailureMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a zero metrics stale timeout is rejected.</summary>
|
||||
[Fact]
|
||||
public void ZeroMetricsStaleTimeout_IsRejected()
|
||||
{
|
||||
@@ -80,6 +87,7 @@ public class HealthMonitoringOptionsValidatorTests
|
||||
Assert.Contains("MetricsStaleTimeout", result.FailureMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a metrics stale timeout shorter than the report interval is rejected.</summary>
|
||||
[Fact]
|
||||
public void MetricsStaleTimeout_ShorterThanReportInterval_IsRejected()
|
||||
{
|
||||
@@ -98,6 +106,8 @@ public class HealthMonitoringOptionsValidatorTests
|
||||
// LocalDb sustained-backlog badge (register row 33)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a non-positive LocalDb backlog threshold is rejected.</summary>
|
||||
/// <param name="threshold">The invalid backlog threshold under test.</param>
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(-1)]
|
||||
@@ -111,6 +121,7 @@ public class HealthMonitoringOptionsValidatorTests
|
||||
Assert.Contains("LocalDbBacklogThreshold", result.FailureMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a zero LocalDb backlog sustained-for window is rejected.</summary>
|
||||
[Fact]
|
||||
public void ZeroLocalDbBacklogSustainedFor_IsRejected()
|
||||
{
|
||||
@@ -123,6 +134,7 @@ public class HealthMonitoringOptionsValidatorTests
|
||||
Assert.Contains("LocalDbBacklogSustainedFor", result.FailureMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a LocalDb backlog sustained-for window shorter than the report interval is rejected.</summary>
|
||||
[Fact]
|
||||
public void LocalDbBacklogSustainedFor_ShorterThanReportInterval_IsRejected()
|
||||
{
|
||||
@@ -139,6 +151,7 @@ public class HealthMonitoringOptionsValidatorTests
|
||||
Assert.Contains("LocalDbBacklogSustainedFor", result.FailureMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the default sustained window comfortably outlasts a purge drain.</summary>
|
||||
[Fact]
|
||||
public void DefaultSustainedWindow_ComfortablyOutlastsAPurgeDrain()
|
||||
{
|
||||
|
||||
@@ -12,7 +12,9 @@ public class HealthReportSenderTests
|
||||
{
|
||||
private class FakeTransport : IHealthReportTransport
|
||||
{
|
||||
/// <summary>Gets the reports captured by <c>SendAsync</c>, in send order.</summary>
|
||||
public List<SiteHealthReport> SentReports { get; } = [];
|
||||
/// <inheritdoc />
|
||||
public Task SendAsync(SiteHealthReport report, CancellationToken cancellationToken)
|
||||
{
|
||||
SentReports.Add(report);
|
||||
@@ -22,22 +24,38 @@ public class HealthReportSenderTests
|
||||
|
||||
private class FakeSiteIdentityProvider : ISiteIdentityProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string SiteId { get; set; } = "test-site";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures emitted log entries so tests can assert that non-fatal failures
|
||||
/// are surfaced (HealthMonitoring-010) rather than silently swallowed.
|
||||
/// are surfaced rather than silently swallowed.
|
||||
/// </summary>
|
||||
private sealed class CapturingLogger<T> : ILogger<T>
|
||||
{
|
||||
public sealed record Entry(LogLevel Level, string Message, Exception? Exception);
|
||||
|
||||
/// <summary>Gets the log entries captured so far.</summary>
|
||||
public List<Entry> Entries { get; } = [];
|
||||
|
||||
/// <summary>Returns no scope; the fake logger does not track scopes.</summary>
|
||||
/// <typeparam name="TState">The type of the scope state.</typeparam>
|
||||
/// <param name="state">The scope state, ignored.</param>
|
||||
/// <returns>Always null or a no-op scope.</returns>
|
||||
public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
|
||||
/// <summary>Reports every log level as enabled.</summary>
|
||||
/// <param name="logLevel">The log level being queried.</param>
|
||||
/// <returns>Always true.</returns>
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
/// <summary>Records the log entry for later assertions.</summary>
|
||||
/// <typeparam name="TState">The type of the log state.</typeparam>
|
||||
/// <param name="logLevel">The severity of the entry.</param>
|
||||
/// <param name="eventId">The event identifier.</param>
|
||||
/// <param name="state">The log state.</param>
|
||||
/// <param name="exception">The associated exception, if any.</param>
|
||||
/// <param name="formatter">Formats the state and exception into the message.</param>
|
||||
public void Log<TState>(
|
||||
LogLevel logLevel, EventId eventId, TState state, Exception? exception,
|
||||
Func<TState, Exception?, string> formatter)
|
||||
@@ -51,6 +69,7 @@ public class HealthReportSenderTests
|
||||
private sealed class NullScope : IDisposable
|
||||
{
|
||||
public static readonly NullScope Instance = new();
|
||||
/// <summary>Releases the resources held by this test class.</summary>
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
@@ -58,11 +77,15 @@ public class HealthReportSenderTests
|
||||
/// <summary>An <see cref="IClusterNodeProvider"/> whose query always throws.</summary>
|
||||
private sealed class ThrowingClusterNodeProvider : IClusterNodeProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool SelfIsPrimary => true;
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<NodeStatus> GetClusterNodes() =>
|
||||
throw new InvalidOperationException("cluster query failed");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that successive reports are sent with monotonically increasing sequence numbers.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SendsReportsWithMonotonicSequenceNumbers()
|
||||
{
|
||||
@@ -107,6 +130,8 @@ public class HealthReportSenderTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the first report sequence number exceeds the startup Unix time in milliseconds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task FirstReportSequenceExceedsStartupUnixMs()
|
||||
{
|
||||
@@ -144,6 +169,8 @@ public class HealthReportSenderTests
|
||||
$"First sequence {transport.SentReports[0].SequenceNumber} should be >= startup epoch {beforeCtor}");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that each report carries a UTC timestamp.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReportsIncludeUtcTimestamp()
|
||||
{
|
||||
@@ -182,12 +209,13 @@ public class HealthReportSenderTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HealthMonitoring-001 regression: the documented "store-and-forward buffer
|
||||
/// Regression: the documented "store-and-forward buffer
|
||||
/// depth" metric (pending messages by category) must actually be populated in
|
||||
/// the emitted report. Previously SetStoreAndForwardDepths had no callers, so
|
||||
/// StoreAndForwardBufferDepths was always empty. The sender must query the S&F
|
||||
/// engine's per-category depth API and include it alongside the parked count.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReportsIncludeStoreAndForwardBufferDepthsFromStorage()
|
||||
{
|
||||
@@ -256,6 +284,7 @@ public class HealthReportSenderTests
|
||||
Status = StoreAndForwardMessageStatus.Pending
|
||||
};
|
||||
|
||||
/// <summary>Verifies that the initial sequence number is seeded from the current Unix time in milliseconds.</summary>
|
||||
[Fact]
|
||||
public void InitialSequenceNumberSeededWithUnixMs()
|
||||
{
|
||||
@@ -276,11 +305,12 @@ public class HealthReportSenderTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HealthMonitoring-010 regression: a failure refreshing cluster nodes is
|
||||
/// Regression: a failure refreshing cluster nodes is
|
||||
/// non-fatal (the report still ships) but must no longer be swallowed by a
|
||||
/// bare <c>catch {}</c> — it must be logged as a warning with the exception so
|
||||
/// persistent degradation is diagnosable.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ClusterNodeRefreshFailure_IsLoggedNotSwallowed()
|
||||
{
|
||||
@@ -325,7 +355,7 @@ public class HealthReportSenderTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HealthMonitoring-017 regression: when the transport's <c>Send</c> throws,
|
||||
/// Regression: when the transport's <c>Send</c> throws,
|
||||
/// the per-interval counters that <see cref="SiteHealthCollector.CollectReport"/>
|
||||
/// just drained via <c>Interlocked.Exchange</c> must be restored back into the
|
||||
/// collector so they roll forward into the next interval rather than being
|
||||
@@ -334,8 +364,8 @@ public class HealthReportSenderTests
|
||||
/// counters at zero.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Extended for arch-review 2026-08-19 P8-1: <c>ScriptRunShedCount</c> is drained
|
||||
/// by <c>CollectReport</c> the exact same way as the five counters above, but
|
||||
/// Also covers <c>ScriptRunShedCount</c>, which is drained
|
||||
/// by <c>CollectReport</c> in the exact same way as the five counters above, but
|
||||
/// <c>AddIntervalCounters</c> used to take only those five — a failed send
|
||||
/// destructively lost the shed count forever. <c>DetachedScriptThreads</c> and
|
||||
/// <c>SiteStreamAlarmDropCount</c> are deliberately NOT exercised here: they are
|
||||
@@ -343,6 +373,7 @@ public class HealthReportSenderTests
|
||||
/// never zeroed by <c>CollectReport</c>), so a failed send loses nothing for them
|
||||
/// to restore.
|
||||
/// </remarks>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SendFailure_PreservesIntervalCountersForNextReport()
|
||||
{
|
||||
@@ -404,13 +435,15 @@ public class HealthReportSenderTests
|
||||
/// <see cref="IHealthReportTransport"/> that throws on the first
|
||||
/// <c>Send</c> call (and only the first), then succeeds. Used by
|
||||
/// <see cref="SendFailure_PreservesIntervalCountersForNextReport"/> to
|
||||
/// verify the HealthMonitoring-017 restore-on-failure path.
|
||||
/// verify the restore-on-failure path.
|
||||
/// </summary>
|
||||
private sealed class FailingThenSucceedingTransport : IHealthReportTransport
|
||||
{
|
||||
private int _callCount;
|
||||
/// <summary>Gets the reports captured by <c>SendAsync</c>, in send order.</summary>
|
||||
public List<SiteHealthReport> SentReports { get; } = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendAsync(SiteHealthReport report, CancellationToken cancellationToken)
|
||||
{
|
||||
var n = Interlocked.Increment(ref _callCount);
|
||||
@@ -424,7 +457,7 @@ public class HealthReportSenderTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HealthMonitoring-006 regression: the sequence-number seed must be derived
|
||||
/// Regression: the sequence-number seed must be derived
|
||||
/// from the injected <see cref="TimeProvider"/> so the Unix-ms seeding strategy
|
||||
/// is deterministically testable and the clock dependency is explicit, rather
|
||||
/// than reading <c>DateTimeOffset.UtcNow</c> directly at field initialization.
|
||||
|
||||
+11
-1
@@ -6,7 +6,7 @@ using ZB.MOM.WW.ScadaBridge.HealthMonitoring.Kpi;
|
||||
namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests.Kpi;
|
||||
|
||||
/// <summary>
|
||||
/// Task 10 (arch-review 04) coverage for <see cref="CentralHealthAuditBacklogProvider"/>.
|
||||
/// Coverage for <see cref="CentralHealthAuditBacklogProvider"/>.
|
||||
/// The provider sums the latest per-site <c>SiteAuditBacklog.PendingCount</c> across the
|
||||
/// in-memory <see cref="ICentralHealthAggregator"/> — the same aggregate the live Audit
|
||||
/// backlog tile shows — so the <c>backlogTotal</c> KPI history stops recording a hardwired
|
||||
@@ -16,6 +16,8 @@ public class CentralHealthAuditBacklogProviderTests
|
||||
{
|
||||
private static readonly DateTime At = new(2026, 6, 17, 12, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
/// <summary>Verifies that the pending backlog total sums the pending count across all sites.</summary>
|
||||
/// <returns>The result.</returns>
|
||||
[Fact]
|
||||
public void GetPendingBacklogTotal_Sums_PendingCount_Across_Sites()
|
||||
{
|
||||
@@ -35,6 +37,8 @@ public class CentralHealthAuditBacklogProviderTests
|
||||
Assert.Equal(13, provider.GetPendingBacklogTotal());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the pending backlog total is zero when no site has reported a backlog.</summary>
|
||||
/// <returns>The result.</returns>
|
||||
[Fact]
|
||||
public void GetPendingBacklogTotal_Returns_Zero_When_No_Backlog_Reported()
|
||||
{
|
||||
@@ -84,17 +88,23 @@ public class CentralHealthAuditBacklogProviderTests
|
||||
/// </summary>
|
||||
private sealed class StubAggregator : ICentralHealthAggregator
|
||||
{
|
||||
/// <summary>Gets the site health states served by the fake.</summary>
|
||||
public Dictionary<string, SiteHealthState> States { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyDictionary<string, SiteHealthState> GetAllSiteStates() => States;
|
||||
|
||||
/// <inheritdoc />
|
||||
public SiteHealthState? GetSiteState(string siteId) =>
|
||||
States.TryGetValue(siteId, out var state) ? state : null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ProcessReport(SiteHealthReport report) => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void MarkHeartbeat(string siteId, DateTimeOffset receivedAt) => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void PruneUnknownSites(IReadOnlyCollection<string> knownSiteIds) => throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
+22
-1
@@ -8,7 +8,7 @@ using ZB.MOM.WW.ScadaBridge.HealthMonitoring.Kpi;
|
||||
namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests.Kpi;
|
||||
|
||||
/// <summary>
|
||||
/// M6 "KPI History & Trends" (K9) coverage for
|
||||
/// "KPI History & Trends" coverage for
|
||||
/// <see cref="SiteHealthKpiSampleSource"/>. The source reads the in-memory
|
||||
/// <see cref="ICentralHealthAggregator"/> and emits per-site
|
||||
/// (<see cref="KpiScopes.Site"/>) <see cref="KpiSample"/> rows for each site that
|
||||
@@ -20,6 +20,7 @@ public class SiteHealthKpiSampleSourceTests
|
||||
private static readonly DateTime CapturedAt =
|
||||
new(2026, 6, 17, 12, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
/// <summary>Verifies that the source identifier is the site health source.</summary>
|
||||
[Fact]
|
||||
public void Source_Is_SiteHealth()
|
||||
{
|
||||
@@ -27,6 +28,8 @@ public class SiteHealthKpiSampleSourceTests
|
||||
Assert.Equal(KpiSources.SiteHealth, source.Source);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that collecting emits every metric for a populated site and nothing for a site with a null report.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CollectAsync_PopulatedSite_EmitsAllMetrics_NullReportSite_EmitsNothing()
|
||||
{
|
||||
@@ -110,6 +113,8 @@ public class SiteHealthKpiSampleSourceTests
|
||||
// LocalDb replication metrics (register row 33)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that collecting emits neither LocalDb metric when the LocalDb fields are null.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CollectAsync_NullLocalDbFields_EmitsNeitherLocalDbMetric()
|
||||
{
|
||||
@@ -134,6 +139,8 @@ public class SiteHealthKpiSampleSourceTests
|
||||
Assert.Equal(12, samples.Count);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that collecting emits both LocalDb metrics for a replicated site.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CollectAsync_ReplicatedSite_EmitsBothLocalDbMetrics()
|
||||
{
|
||||
@@ -154,6 +161,8 @@ public class SiteHealthKpiSampleSourceTests
|
||||
Assert.Equal(14, samples.Count);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that collecting still emits a zero backlog value.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CollectAsync_ZeroBacklog_IsStillEmitted()
|
||||
{
|
||||
@@ -171,6 +180,8 @@ public class SiteHealthKpiSampleSourceTests
|
||||
Assert.Equal(0, samples.Single(s => s.Metric == "localDbOplogBacklog").Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that collecting emits zero rather than omitting the metric when replication is disconnected.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CollectAsync_DisconnectedReplication_EmitsZeroNotAbsent()
|
||||
{
|
||||
@@ -188,6 +199,8 @@ public class SiteHealthKpiSampleSourceTests
|
||||
Assert.Equal(0, samples.Single(s => s.Metric == "localDbReplicationConnected").Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that collecting emits zero pending audit backlog when the audit backlog is null.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CollectAsync_NullAuditBacklog_EmitsZeroAuditBacklogPending()
|
||||
{
|
||||
@@ -202,6 +215,8 @@ public class SiteHealthKpiSampleSourceTests
|
||||
Assert.Equal(0, samples.Single(s => s.Metric == "auditBacklogPending").Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that collecting returns an empty list when no site has a report.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CollectAsync_NoSitesWithReports_ReturnsEmptyList()
|
||||
{
|
||||
@@ -238,19 +253,25 @@ public class SiteHealthKpiSampleSourceTests
|
||||
/// </summary>
|
||||
private sealed class StubAggregator : ICentralHealthAggregator
|
||||
{
|
||||
/// <summary>Gets the canned per-site health states returned by the fake.</summary>
|
||||
public Dictionary<string, SiteHealthState> States { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyDictionary<string, SiteHealthState> GetAllSiteStates() => States;
|
||||
|
||||
/// <inheritdoc />
|
||||
public SiteHealthState? GetSiteState(string siteId) =>
|
||||
States.TryGetValue(siteId, out var state) ? state : null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ProcessReport(SiteHealthReport report) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void MarkHeartbeat(string siteId, DateTimeOffset receivedAt) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void PruneUnknownSites(IReadOnlyCollection<string> knownSiteIds) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ public sealed class LocalDbBacklogSustainedTests
|
||||
{
|
||||
private const long Threshold = 1000;
|
||||
|
||||
/// <summary>Verifies that a backlog above the threshold starts the above-threshold clock.</summary>
|
||||
[Fact]
|
||||
public void BacklogAboveThreshold_StartsTheClock()
|
||||
{
|
||||
@@ -32,6 +33,7 @@ public sealed class LocalDbBacklogSustainedTests
|
||||
Assert.Equal(start, aggregator.GetSiteState("site-a")!.LocalDbBacklogAboveSince);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a backlog that stays above the threshold keeps the original start instant.</summary>
|
||||
[Fact]
|
||||
public void BacklogStayingAbove_KeepsTheOriginalStartInstant()
|
||||
{
|
||||
@@ -47,6 +49,7 @@ public sealed class LocalDbBacklogSustainedTests
|
||||
Assert.Equal(start, aggregator.GetSiteState("site-a")!.LocalDbBacklogAboveSince);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a backlog at or below the threshold clears the clock.</summary>
|
||||
[Fact]
|
||||
public void BacklogAtOrBelowThreshold_ClearsTheClock()
|
||||
{
|
||||
@@ -61,6 +64,7 @@ public sealed class LocalDbBacklogSustainedTests
|
||||
Assert.Null(aggregator.GetSiteState("site-a")!.LocalDbBacklogAboveSince);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a backlog exactly at the threshold does not start the clock.</summary>
|
||||
[Fact]
|
||||
public void BacklogExactlyAtThreshold_DoesNotStartTheClock()
|
||||
{
|
||||
@@ -73,6 +77,7 @@ public sealed class LocalDbBacklogSustainedTests
|
||||
Assert.Null(aggregator.GetSiteState("site-a")!.LocalDbBacklogAboveSince);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unreplicated site reporting a null backlog never accumulates an anchor.</summary>
|
||||
[Fact]
|
||||
public void UnreplicatedSite_NeverAccumulatesAnAnchor()
|
||||
{
|
||||
@@ -88,6 +93,7 @@ public sealed class LocalDbBacklogSustainedTests
|
||||
Assert.Null(aggregator.GetSiteState("site-b")!.LocalDbBacklogAboveSince);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null backlog in the middle of a climb leaves the anchor untouched.</summary>
|
||||
[Fact]
|
||||
public void NullBacklogMidClimb_LeavesTheAnchorUntouched()
|
||||
{
|
||||
@@ -104,6 +110,7 @@ public sealed class LocalDbBacklogSustainedTests
|
||||
Assert.Equal(start, aggregator.GetSiteState("site-a")!.LocalDbBacklogAboveSince);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the very first report for a new site can start the clock.</summary>
|
||||
[Fact]
|
||||
public void FirstReportForANewSite_CanStartTheClock()
|
||||
{
|
||||
@@ -117,6 +124,7 @@ public sealed class LocalDbBacklogSustainedTests
|
||||
Assert.Equal(start, aggregator.GetSiteState("brand-new")!.LocalDbBacklogAboveSince);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the backlog threshold is configurable.</summary>
|
||||
[Fact]
|
||||
public void ThresholdIsConfigurable()
|
||||
{
|
||||
@@ -127,6 +135,7 @@ public sealed class LocalDbBacklogSustainedTests
|
||||
Assert.NotNull(aggregator.GetSiteState("site-a")!.LocalDbBacklogAboveSince);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a stale out-of-order report does not disturb the anchor.</summary>
|
||||
[Fact]
|
||||
public void StaleReport_DoesNotDisturbTheAnchor()
|
||||
{
|
||||
|
||||
+9
-1
@@ -32,10 +32,11 @@ public class LocalDbReplicationStatusReporterTests
|
||||
logger: NullLogger<LocalDbReplicationStatusReporter>.Instance,
|
||||
refreshInterval: TimeSpan.FromHours(1));
|
||||
|
||||
/// <summary>Verifies that probing an unconfigured replication node publishes null for both fields.</summary>
|
||||
[Fact]
|
||||
public void Probe_ReplicationUnconfigured_PublishesNullForBothFields()
|
||||
{
|
||||
// The row-80 regression. The inert engine's false/0 must NOT reach the report.
|
||||
// The inert-engine regression. The inert engine's false/0 must NOT reach the report.
|
||||
var collector = new SiteHealthCollector();
|
||||
using var reporter = Build(collector, replicationConfigured: false);
|
||||
|
||||
@@ -46,6 +47,7 @@ public class LocalDbReplicationStatusReporterTests
|
||||
Assert.Null(report.LocalDbOplogBacklog);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that probing an unconfigured replication node does not read the sync status providers.</summary>
|
||||
[Fact]
|
||||
public void Probe_ReplicationUnconfigured_DoesNotReadTheSyncStatusProviders()
|
||||
{
|
||||
@@ -69,6 +71,7 @@ public class LocalDbReplicationStatusReporterTests
|
||||
Assert.Equal(0, backlogReads);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a configured but disconnected node publishes false rather than null.</summary>
|
||||
[Fact]
|
||||
public void Probe_ReplicationConfiguredButDisconnected_PublishesFalse_NotNull()
|
||||
{
|
||||
@@ -86,6 +89,7 @@ public class LocalDbReplicationStatusReporterTests
|
||||
Assert.Equal(4200L, report.LocalDbOplogBacklog);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a configured and connected node publishes true.</summary>
|
||||
[Fact]
|
||||
public void Probe_ReplicationConfiguredAndConnected_PublishesTrue()
|
||||
{
|
||||
@@ -100,6 +104,7 @@ public class LocalDbReplicationStatusReporterTests
|
||||
Assert.Equal(0L, report.LocalDbOplogBacklog);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a configured node preserves a null backlog as unknown.</summary>
|
||||
[Fact]
|
||||
public void Probe_ReplicationConfigured_PreservesANullBacklogAsUnknown()
|
||||
{
|
||||
@@ -117,6 +122,7 @@ public class LocalDbReplicationStatusReporterTests
|
||||
Assert.Null(report.LocalDbOplogBacklog);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a throwing provider leaves the previous snapshot intact.</summary>
|
||||
[Fact]
|
||||
public void Probe_ProviderThrows_LeavesThePreviousSnapshotIntact()
|
||||
{
|
||||
@@ -143,6 +149,7 @@ public class LocalDbReplicationStatusReporterTests
|
||||
Assert.Equal(7L, report.LocalDbOplogBacklog);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that flipping the replication gate on stops the null reporting.</summary>
|
||||
[Fact]
|
||||
public void Probe_GateFlippedOn_StopsReportingNull()
|
||||
{
|
||||
@@ -169,6 +176,7 @@ public class LocalDbReplicationStatusReporterTests
|
||||
Assert.Equal(12L, report.LocalDbOplogBacklog);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null replication-configured provider causes the constructor to throw.</summary>
|
||||
[Fact]
|
||||
public void Constructor_NullReplicationConfiguredProvider_Throws()
|
||||
{
|
||||
|
||||
+4
-1
@@ -3,17 +3,19 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.Health;
|
||||
namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Review finding M1 — <c>SiteHealthReport.SecretMirrorStatuses</c> carries every site node's
|
||||
/// <c>SiteHealthReport.SecretMirrorStatuses</c> carries every site node's
|
||||
/// deployed-secrets mirror failures (the standby's included) on the active node's report.
|
||||
/// </summary>
|
||||
public class SecretMirrorStatusHealthFieldTests
|
||||
{
|
||||
/// <summary>Verifies that a report collected before any status was supplied leaves the field null.</summary>
|
||||
[Fact]
|
||||
public void CollectReport_NeverTold_LeavesTheFieldNull()
|
||||
{
|
||||
Assert.Null(new SiteHealthCollector().CollectReport("TestSite").SecretMirrorStatuses);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a collected report carries the latest snapshot without draining it.</summary>
|
||||
[Fact]
|
||||
public void CollectReport_CarriesTheLatestSnapshot_AndDoesNotDrainIt()
|
||||
{
|
||||
@@ -33,6 +35,7 @@ public class SecretMirrorStatusHealthFieldTests
|
||||
Assert.Equal("S1", Assert.Single(first[1].Failures).SecretName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that setting the status to null resets the field to no data.</summary>
|
||||
[Fact]
|
||||
public void SetNull_ResetsToNoData()
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Bundle E (M6-T6) regression coverage. The site-side audit-log SQLite writer
|
||||
/// Regression coverage. The site-side audit-log SQLite writer
|
||||
/// exposes a backlog snapshot (<c>SiteAuditBacklogSnapshot</c>) via the
|
||||
/// <c>ISiteAuditQueue.GetBacklogStatsAsync</c> surface. A periodic
|
||||
/// <c>SiteAuditBacklogReporter</c> hosted service polls that snapshot and
|
||||
@@ -18,6 +18,7 @@ public class SiteAuditBacklogMetricTests
|
||||
{
|
||||
private readonly SiteHealthCollector _collector = new();
|
||||
|
||||
/// <summary>Verifies that a report collected after an update includes the backlog.</summary>
|
||||
[Fact]
|
||||
public void Update_Then_CollectReport_IncludesBacklog()
|
||||
{
|
||||
@@ -33,6 +34,7 @@ public class SiteAuditBacklogMetricTests
|
||||
Assert.Equal(snapshot, report.SiteAuditBacklog);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the report payload includes the site audit backlog as null by default.</summary>
|
||||
[Fact]
|
||||
public void Report_Payload_Includes_SiteAuditBacklog_AsNullByDefault()
|
||||
{
|
||||
@@ -43,6 +45,7 @@ public class SiteAuditBacklogMetricTests
|
||||
Assert.Null(report.SiteAuditBacklog);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that collecting a report does not reset the site audit backlog.</summary>
|
||||
[Fact]
|
||||
public void CollectReport_DoesNotReset_SiteAuditBacklog()
|
||||
{
|
||||
@@ -64,6 +67,7 @@ public class SiteAuditBacklogMetricTests
|
||||
Assert.Equal(snapshot, second.SiteAuditBacklog);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that updating with a null snapshot throws an argument-null exception.</summary>
|
||||
[Fact]
|
||||
public void Update_With_Null_Throws_ArgumentNullException()
|
||||
{
|
||||
|
||||
+3
-1
@@ -1,7 +1,7 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Bundle G (M2-T11) regression coverage. The site-side Audit Log writer chain
|
||||
/// Regression coverage for the site audit write failure metric. The site-side Audit Log writer chain
|
||||
/// (FallbackAuditWriter) increments <see cref="IAuditWriteFailureCounter"/>
|
||||
/// every time the primary SQLite writer throws. Bundle G bridges that counter
|
||||
/// into the Site Health Monitoring report payload as <c>SiteAuditWriteFailures</c>
|
||||
@@ -12,6 +12,7 @@ public class SiteAuditWriteFailuresMetricTests
|
||||
{
|
||||
private readonly SiteHealthCollector _collector = new();
|
||||
|
||||
/// <summary>Verifies that incrementing the counter three times makes it report 3.</summary>
|
||||
[Fact]
|
||||
public void Increment_Three_Times_Counter_Reports_3()
|
||||
{
|
||||
@@ -24,6 +25,7 @@ public class SiteAuditWriteFailuresMetricTests
|
||||
Assert.Equal(3, report.SiteAuditWriteFailures);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the report payload includes the site audit write failure count as zero by default.</summary>
|
||||
[Fact]
|
||||
public void Report_Payload_Includes_SiteAuditWriteFailures_AsZeroByDefault()
|
||||
{
|
||||
|
||||
+8
-2
@@ -3,17 +3,19 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// M2.16 (#30) — unit tests for <see cref="SiteEventLogFailureCountReporter"/>.
|
||||
/// Unit tests for <see cref="SiteEventLogFailureCountReporter"/>.
|
||||
/// Verifies that the poller reads the count provided by the
|
||||
/// <see cref="Func{TResult}"/> delegate and pushes it into
|
||||
/// <see cref="ISiteHealthCollector.SetSiteEventLogWriteFailures"/>.
|
||||
/// </summary>
|
||||
public class SiteEventLogFailureCountReporterTests
|
||||
{
|
||||
/// <summary>Verifies that stopping the reporter after it has been disposed does not throw.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task StopAsync_AfterDispose_DoesNotThrow()
|
||||
{
|
||||
// Regression (Gitea #15 follow-up): Dispose tears down the CTS StopAsync
|
||||
// Regression: Dispose tears down the CTS StopAsync
|
||||
// cancels, and the host does not guarantee StopAsync is driven before the DI
|
||||
// container is disposed. Cancel() on a disposed CTS throws, and letting that
|
||||
// escape an IHostedService aborts the host's whole shutdown sequence.
|
||||
@@ -31,6 +33,8 @@ public class SiteEventLogFailureCountReporterTests
|
||||
reporter.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that starting the reporter immediately probes the failed-write count.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task StartAsync_ImmediatelyProbes_FailedWriteCount()
|
||||
{
|
||||
@@ -61,6 +65,8 @@ public class SiteEventLogFailureCountReporterTests
|
||||
await reporter.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the reporter pushes the latest failed-write count on each tick.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task StartAsync_PushesLatestCount_OnEachTick()
|
||||
{
|
||||
|
||||
+5
-1
@@ -1,7 +1,7 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// M2.16 (#30) regression coverage. <see cref="ISiteEventLogger.FailedWriteCount"/>
|
||||
/// Regression coverage. <see cref="ISiteEventLogger.FailedWriteCount"/>
|
||||
/// is a cumulative (point-in-time) counter. A periodic
|
||||
/// <c>SiteEventLogFailureCountReporter</c> hosted service polls the count and
|
||||
/// pushes it into the collector via
|
||||
@@ -15,6 +15,7 @@ public class SiteEventLogWriteFailuresMetricTests
|
||||
{
|
||||
private readonly SiteHealthCollector _collector = new();
|
||||
|
||||
/// <summary>Verifies that a count that was set appears in the collected report.</summary>
|
||||
[Fact]
|
||||
public void Set_Then_CollectReport_IncludesCount()
|
||||
{
|
||||
@@ -25,6 +26,7 @@ public class SiteEventLogWriteFailuresMetricTests
|
||||
Assert.Equal(17L, report.SiteEventLogWriteFailures);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the report payload includes the site event log write failure count, defaulting to zero.</summary>
|
||||
[Fact]
|
||||
public void Report_Payload_Includes_SiteEventLogWriteFailures_AsZeroByDefault()
|
||||
{
|
||||
@@ -33,6 +35,7 @@ public class SiteEventLogWriteFailuresMetricTests
|
||||
Assert.Equal(0L, report.SiteEventLogWriteFailures);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that collecting a report does not reset the site event log write failure count.</summary>
|
||||
[Fact]
|
||||
public void CollectReport_DoesNotReset_SiteEventLogWriteFailures()
|
||||
{
|
||||
@@ -49,6 +52,7 @@ public class SiteEventLogWriteFailuresMetricTests
|
||||
Assert.Equal(42L, second.SiteEventLogWriteFailures);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that setting the count overwrites the previous value.</summary>
|
||||
[Fact]
|
||||
public void Set_Overwrites_Previous_Value()
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ public class SiteHealthCollectorTests
|
||||
{
|
||||
private readonly SiteHealthCollector _collector = new();
|
||||
|
||||
/// <summary>Verifies that a report has zero counters when no errors have been recorded.</summary>
|
||||
[Fact]
|
||||
public void CollectReport_ReturnsZeroCounters_WhenNoErrorsRecorded()
|
||||
{
|
||||
@@ -17,6 +18,7 @@ public class SiteHealthCollectorTests
|
||||
Assert.Equal(0, report.DeadLetterCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that script errors accumulate between reports.</summary>
|
||||
[Fact]
|
||||
public void IncrementScriptError_AccumulatesBetweenReports()
|
||||
{
|
||||
@@ -28,6 +30,7 @@ public class SiteHealthCollectorTests
|
||||
Assert.Equal(3, report.ScriptErrorCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that alarm errors accumulate between reports.</summary>
|
||||
[Fact]
|
||||
public void IncrementAlarmError_AccumulatesBetweenReports()
|
||||
{
|
||||
@@ -38,6 +41,7 @@ public class SiteHealthCollectorTests
|
||||
Assert.Equal(2, report.AlarmEvaluationErrorCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a report carries the script scheduler statistics.</summary>
|
||||
[Fact]
|
||||
public void CollectReport_CarriesScriptSchedulerStats()
|
||||
{
|
||||
@@ -48,6 +52,7 @@ public class SiteHealthCollectorTests
|
||||
Assert.Equal(42.5, report.ScriptOldestBusyAgeSeconds);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that script scheduler statistics default to zero and null.</summary>
|
||||
[Fact]
|
||||
public void CollectReport_ScriptSchedulerStats_DefaultToZeroAndNull()
|
||||
{
|
||||
@@ -57,6 +62,7 @@ public class SiteHealthCollectorTests
|
||||
Assert.Null(report.ScriptOldestBusyAgeSeconds);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that dead letters accumulate between reports.</summary>
|
||||
[Fact]
|
||||
public void IncrementDeadLetter_AccumulatesBetweenReports()
|
||||
{
|
||||
@@ -66,6 +72,7 @@ public class SiteHealthCollectorTests
|
||||
Assert.Equal(1, report.DeadLetterCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that collecting a report resets the counters.</summary>
|
||||
[Fact]
|
||||
public void CollectReport_ResetsCounters_AfterCollection()
|
||||
{
|
||||
@@ -84,6 +91,7 @@ public class SiteHealthCollectorTests
|
||||
Assert.Equal(0, second.DeadLetterCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a connection health update is reflected in the report.</summary>
|
||||
[Fact]
|
||||
public void UpdateConnectionHealth_ReflectedInReport()
|
||||
{
|
||||
@@ -97,6 +105,7 @@ public class SiteHealthCollectorTests
|
||||
Assert.Equal(ConnectionHealth.Disconnected, report.DataConnectionStatuses["opc-2"]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that connection health is not reset after a report is collected.</summary>
|
||||
[Fact]
|
||||
public void ConnectionHealth_NotResetAfterCollect()
|
||||
{
|
||||
@@ -109,6 +118,7 @@ public class SiteHealthCollectorTests
|
||||
Assert.Equal(ConnectionHealth.Connected, second.DataConnectionStatuses["opc-1"]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that removing a connection removes it from the report.</summary>
|
||||
[Fact]
|
||||
public void RemoveConnection_RemovesFromReport()
|
||||
{
|
||||
@@ -121,6 +131,7 @@ public class SiteHealthCollectorTests
|
||||
Assert.Empty(report.TagResolutionCounts);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a tag resolution update is reflected in the report.</summary>
|
||||
[Fact]
|
||||
public void UpdateTagResolution_ReflectedInReport()
|
||||
{
|
||||
@@ -133,6 +144,7 @@ public class SiteHealthCollectorTests
|
||||
Assert.Equal(45, report.TagResolutionCounts["opc-1"].SuccessfullyResolved);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that store-and-forward buffer depths default to empty when the setter was never called.</summary>
|
||||
[Fact]
|
||||
public void StoreAndForwardBufferDepths_DefaultsToEmpty_WhenSetterNotCalled()
|
||||
{
|
||||
@@ -140,6 +152,7 @@ public class SiteHealthCollectorTests
|
||||
Assert.Empty(report.StoreAndForwardBufferDepths);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a report includes a UTC timestamp.</summary>
|
||||
[Fact]
|
||||
public void CollectReport_IncludesUtcTimestamp()
|
||||
{
|
||||
@@ -151,7 +164,7 @@ public class SiteHealthCollectorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HealthMonitoring-016 regression: <see cref="SiteHealthCollector.CollectReport"/>
|
||||
/// Regression: <see cref="SiteHealthCollector.CollectReport"/>
|
||||
/// must stamp <c>ReportTimestamp</c> from an injected <see cref="TimeProvider"/>
|
||||
/// (consistent with the rest of the module), not directly from
|
||||
/// <c>DateTimeOffset.UtcNow</c>, so the report timestamp is deterministically
|
||||
@@ -168,6 +181,7 @@ public class SiteHealthCollectorTests
|
||||
Assert.Equal(fixedInstant, report.ReportTimestamp);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a report sequence number is zero because the caller assigns it.</summary>
|
||||
[Fact]
|
||||
public void CollectReport_SequenceNumberIsZero_CallerAssignsIt()
|
||||
{
|
||||
@@ -175,10 +189,11 @@ public class SiteHealthCollectorTests
|
||||
Assert.Equal(0, report.SequenceNumber);
|
||||
}
|
||||
|
||||
// HealthMonitoring-009 regression: the remaining collector setters had no
|
||||
// Regression coverage: the remaining collector setters had no
|
||||
// "reflected in report" coverage. The following tests verify each setter's
|
||||
// value reaches CollectReport output.
|
||||
|
||||
/// <summary>Verifies that the cluster nodes are reflected in the report.</summary>
|
||||
[Fact]
|
||||
public void SetClusterNodes_ReflectedInReport()
|
||||
{
|
||||
@@ -196,6 +211,7 @@ public class SiteHealthCollectorTests
|
||||
Assert.Equal("node-a", report.ClusterNodes[0].Hostname);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the instance counts are reflected in the report.</summary>
|
||||
[Fact]
|
||||
public void SetInstanceCounts_ReflectedInReport()
|
||||
{
|
||||
@@ -208,6 +224,7 @@ public class SiteHealthCollectorTests
|
||||
Assert.Equal(3, report.DisabledInstanceCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the parked message count is reflected in the report.</summary>
|
||||
[Fact]
|
||||
public void SetParkedMessageCount_ReflectedInReport()
|
||||
{
|
||||
@@ -218,6 +235,7 @@ public class SiteHealthCollectorTests
|
||||
Assert.Equal(42, report.ParkedMessageCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the node hostname is reflected in the report.</summary>
|
||||
[Fact]
|
||||
public void SetNodeHostname_ReflectedInReport()
|
||||
{
|
||||
@@ -228,6 +246,7 @@ public class SiteHealthCollectorTests
|
||||
Assert.Equal("site-host-1", report.NodeHostname);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the active-node setting is reflected in the node role.</summary>
|
||||
[Fact]
|
||||
public void SetActiveNode_ReflectedInNodeRole()
|
||||
{
|
||||
@@ -240,6 +259,7 @@ public class SiteHealthCollectorTests
|
||||
Assert.False(_collector.IsActiveNode);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a tag quality update is reflected in the report.</summary>
|
||||
[Fact]
|
||||
public void UpdateTagQuality_ReflectedInReport()
|
||||
{
|
||||
@@ -254,6 +274,7 @@ public class SiteHealthCollectorTests
|
||||
Assert.Equal(5, quality.Uncertain);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a connection endpoint update is reflected in the report.</summary>
|
||||
[Fact]
|
||||
public void UpdateConnectionEndpoint_ReflectedInReport()
|
||||
{
|
||||
@@ -265,6 +286,7 @@ public class SiteHealthCollectorTests
|
||||
Assert.Equal("opc.tcp://plc-1:4840", report.DataConnectionEndpoints!["opc-1"]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the store-and-forward depths are reflected in the report.</summary>
|
||||
[Fact]
|
||||
public void SetStoreAndForwardDepths_ReflectedInReport()
|
||||
{
|
||||
@@ -280,6 +302,8 @@ public class SiteHealthCollectorTests
|
||||
Assert.Equal(2, report.StoreAndForwardBufferDepths["Notification"]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that concurrent increments are counted without loss.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ThreadSafety_ConcurrentIncrements()
|
||||
{
|
||||
|
||||
+6
-1
@@ -7,13 +7,14 @@ namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests;
|
||||
/// <remarks>
|
||||
/// The skip used to be a single boot Warning and nothing else, so a node that lost its whole
|
||||
/// self-heal path went on reporting itself perfectly healthy. The field is tri-state and the
|
||||
/// three cases must stay distinct: null = not reported (central self-report, or a pre-row-73
|
||||
/// three cases must stay distinct: null = not reported (central self-report, or a older
|
||||
/// site build), true = running, false = skipped because no <c>IDeploymentConfigFetcher</c>
|
||||
/// was available. Collapsing null into false would badge every site the moment central is
|
||||
/// upgraded ahead of them.
|
||||
/// </remarks>
|
||||
public class StartupReconcilerHealthFieldTests
|
||||
{
|
||||
/// <summary>Verifies the case: collect Report, never Told, leaves The Field Null.</summary>
|
||||
[Fact]
|
||||
public void CollectReport_NeverTold_LeavesTheFieldNull()
|
||||
{
|
||||
@@ -26,6 +27,7 @@ public class StartupReconcilerHealthFieldTests
|
||||
Assert.Null(report.StartupReconcilerActive);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: collect Report, reconciler Spawned, reports True.</summary>
|
||||
[Fact]
|
||||
public void CollectReport_ReconcilerSpawned_ReportsTrue()
|
||||
{
|
||||
@@ -36,6 +38,7 @@ public class StartupReconcilerHealthFieldTests
|
||||
Assert.True(collector.CollectReport("TestSite").StartupReconcilerActive);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: collect Report, reconciler Skipped, reports False, not Null.</summary>
|
||||
[Fact]
|
||||
public void CollectReport_ReconcilerSkipped_ReportsFalse_NotNull()
|
||||
{
|
||||
@@ -50,6 +53,7 @@ public class StartupReconcilerHealthFieldTests
|
||||
Assert.False(report.StartupReconcilerActive);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: collect Report, is Point In Time, not Drained On Collect.</summary>
|
||||
[Fact]
|
||||
public void CollectReport_IsPointInTime_NotDrainedOnCollect()
|
||||
{
|
||||
@@ -64,6 +68,7 @@ public class StartupReconcilerHealthFieldTests
|
||||
Assert.False(second.StartupReconcilerActive);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: set Startup Reconciler Active, is Not Write Once.</summary>
|
||||
[Fact]
|
||||
public void SetStartupReconcilerActive_IsNotWriteOnce()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user