test(commons): realign enum-count guards (AuditKind/AuditChannel/DataType) + derace StaleTagMonitor timer tests (#228, pre-existing)
This commit is contained in:
@@ -5,7 +5,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
|||||||
public class EnumTests
|
public class EnumTests
|
||||||
{
|
{
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(typeof(DataType), new[] { "Boolean", "Int32", "Float", "Double", "String", "DateTime", "Binary" })]
|
[InlineData(typeof(DataType), new[] { "Boolean", "Int32", "Float", "Double", "String", "DateTime", "Binary", "List" })]
|
||||||
[InlineData(typeof(InstanceState), new[] { "NotDeployed", "Enabled", "Disabled" })]
|
[InlineData(typeof(InstanceState), new[] { "NotDeployed", "Enabled", "Disabled" })]
|
||||||
[InlineData(typeof(DeploymentStatus), new[] { "Pending", "InProgress", "Success", "Failed" })]
|
[InlineData(typeof(DeploymentStatus), new[] { "Pending", "InProgress", "Success", "Failed" })]
|
||||||
[InlineData(typeof(AlarmState), new[] { "Active", "Normal" })]
|
[InlineData(typeof(AlarmState), new[] { "Active", "Normal" })]
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public class AuditEnumTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void AuditChannel_HasExactlyExpectedMembers()
|
public void AuditChannel_HasExactlyExpectedMembers()
|
||||||
{
|
{
|
||||||
var expected = new[] { "ApiOutbound", "DbOutbound", "Notification", "ApiInbound" };
|
var expected = new[] { "ApiOutbound", "DbOutbound", "Notification", "ApiInbound", "SecuredWrite" };
|
||||||
var actual = Enum.GetValues(typeof(AuditChannel))
|
var actual = Enum.GetValues(typeof(AuditChannel))
|
||||||
.Cast<AuditChannel>()
|
.Cast<AuditChannel>()
|
||||||
.Select(x => x.ToString())
|
.Select(x => x.ToString())
|
||||||
@@ -23,20 +23,21 @@ public class AuditEnumTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AuditKind_HasExactlyTenExpectedMembers()
|
public void AuditKind_HasExactlyFourteenExpectedMembers()
|
||||||
{
|
{
|
||||||
var expected = new[]
|
var expected = new[]
|
||||||
{
|
{
|
||||||
"ApiCall", "ApiCallCached", "DbWrite", "DbWriteCached",
|
"ApiCall", "ApiCallCached", "DbWrite", "DbWriteCached",
|
||||||
"NotifySend", "NotifyDeliver", "InboundRequest", "InboundAuthFailure",
|
"NotifySend", "NotifyDeliver", "InboundRequest", "InboundAuthFailure",
|
||||||
"CachedSubmit", "CachedResolve",
|
"CachedSubmit", "CachedResolve",
|
||||||
|
"SecuredWriteSubmit", "SecuredWriteApprove", "SecuredWriteReject", "SecuredWriteExecute",
|
||||||
};
|
};
|
||||||
var actual = Enum.GetValues(typeof(AuditKind))
|
var actual = Enum.GetValues(typeof(AuditKind))
|
||||||
.Cast<AuditKind>()
|
.Cast<AuditKind>()
|
||||||
.Select(x => x.ToString())
|
.Select(x => x.ToString())
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
Assert.Equal(10, actual.Length);
|
Assert.Equal(14, actual.Length);
|
||||||
Assert.Equal(expected, actual);
|
Assert.Equal(expected, actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks both StaleTagMonitor test classes as non-parallel with each other and with the
|
||||||
|
/// rest of the test suite. This prevents CPU-contention-induced timing flakiness in tests
|
||||||
|
/// that rely on wall-clock timer deadlines (MaxSilence values in the 30–200ms range).
|
||||||
|
/// xUnit runs all tests in the same [Collection] sequentially.
|
||||||
|
/// </summary>
|
||||||
|
[CollectionDefinition("StaleTagMonitor", DisableParallelization = true)]
|
||||||
|
public class StaleTagMonitorCollection { }
|
||||||
@@ -5,6 +5,8 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Regression tests for Commons-001: the check-then-act race between the timer
|
/// Regression tests for Commons-001: the check-then-act race between the timer
|
||||||
/// callback (<c>OnTimerElapsed</c>) and <c>OnValueReceived</c> / <c>Stop</c> / <c>Start</c>.
|
/// callback (<c>OnTimerElapsed</c>) and <c>OnValueReceived</c> / <c>Stop</c> / <c>Start</c>.
|
||||||
|
/// Placed in the same non-parallel collection as <see cref="StaleTagMonitorTests"/> to
|
||||||
|
/// avoid CPU contention from other timer-based tests running concurrently.
|
||||||
///
|
///
|
||||||
/// The original implementation guarded firing with a single <c>volatile bool</c> that
|
/// The original implementation guarded firing with a single <c>volatile bool</c> that
|
||||||
/// was both read by the callback and reset by the caller threads. Because the
|
/// was both read by the callback and reset by the caller threads. Because the
|
||||||
@@ -16,6 +18,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
|||||||
/// These tests use the internal <c>CallbackEnteredHook</c> seam to deterministically
|
/// These tests use the internal <c>CallbackEnteredHook</c> seam to deterministically
|
||||||
/// interleave a caller-thread operation with an in-flight callback.
|
/// interleave a caller-thread operation with an in-flight callback.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Collection("StaleTagMonitor")]
|
||||||
public class StaleTagMonitorRaceTests
|
public class StaleTagMonitorRaceTests
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -2,8 +2,33 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
|||||||
|
|
||||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Both StaleTagMonitor test classes are placed in the same non-parallel collection
|
||||||
|
/// to prevent wall-clock contention when the full suite runs under load.
|
||||||
|
/// Within each test, fixed-delay assertions are replaced with poll-until-condition
|
||||||
|
/// loops that tolerate CPU jitter on a busy machine.
|
||||||
|
/// </summary>
|
||||||
|
[Collection("StaleTagMonitor")]
|
||||||
public class StaleTagMonitorTests
|
public class StaleTagMonitorTests
|
||||||
{
|
{
|
||||||
|
// Generous poll timeout: tests use MaxSilence values of 50–200ms; we allow up to
|
||||||
|
// 5 seconds of total wait before declaring failure. This absorbs any scheduler lag
|
||||||
|
// that would previously cause a tight Task.Delay window to expire before the timer
|
||||||
|
// had a chance to fire.
|
||||||
|
private static readonly TimeSpan PollTimeout = TimeSpan.FromSeconds(5);
|
||||||
|
private static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(10);
|
||||||
|
|
||||||
|
private static async Task<bool> PollUntilAsync(Func<bool> condition, TimeSpan timeout)
|
||||||
|
{
|
||||||
|
var deadline = DateTime.UtcNow + timeout;
|
||||||
|
while (DateTime.UtcNow < deadline)
|
||||||
|
{
|
||||||
|
if (condition()) return true;
|
||||||
|
await Task.Delay(PollInterval);
|
||||||
|
}
|
||||||
|
return condition(); // final check
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Constructor_ZeroTimeSpan_Throws()
|
public void Constructor_ZeroTimeSpan_Throws()
|
||||||
{
|
{
|
||||||
@@ -24,7 +49,8 @@ public class StaleTagMonitorTests
|
|||||||
monitor.Stale += () => Interlocked.Increment(ref staleCount);
|
monitor.Stale += () => Interlocked.Increment(ref staleCount);
|
||||||
monitor.Start();
|
monitor.Start();
|
||||||
|
|
||||||
await Task.Delay(300);
|
var fired = await PollUntilAsync(() => staleCount >= 1, PollTimeout);
|
||||||
|
Assert.True(fired, "Stale event did not fire within the generous timeout.");
|
||||||
Assert.Equal(1, staleCount);
|
Assert.Equal(1, staleCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +62,13 @@ public class StaleTagMonitorTests
|
|||||||
monitor.Stale += () => Interlocked.Increment(ref staleCount);
|
monitor.Stale += () => Interlocked.Increment(ref staleCount);
|
||||||
monitor.Start();
|
monitor.Start();
|
||||||
|
|
||||||
await Task.Delay(300);
|
// Wait for the first (and only) fire, then hold for a bit to confirm no second fire.
|
||||||
|
var fired = await PollUntilAsync(() => staleCount >= 1, PollTimeout);
|
||||||
|
Assert.True(fired, "Stale event did not fire within the generous timeout.");
|
||||||
|
|
||||||
|
// Hold long enough to confirm no second fire — the one-shot timer design means
|
||||||
|
// a second fire can only happen if OnValueReceived is called; we do not call it.
|
||||||
|
await Task.Delay(200);
|
||||||
Assert.Equal(1, staleCount);
|
Assert.Equal(1, staleCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,14 +80,16 @@ public class StaleTagMonitorTests
|
|||||||
monitor.Stale += () => Interlocked.Increment(ref staleCount);
|
monitor.Stale += () => Interlocked.Increment(ref staleCount);
|
||||||
monitor.Start();
|
monitor.Start();
|
||||||
|
|
||||||
// Keep resetting before the 200ms deadline
|
// Keep resetting before the 200ms deadline — use the monitor's own MaxSilence
|
||||||
|
// divided by 2 as the reset cadence so timing margin is relative, not absolute.
|
||||||
|
var resetInterval = (int)(monitor.MaxSilence.TotalMilliseconds / 2);
|
||||||
for (int i = 0; i < 5; i++)
|
for (int i = 0; i < 5; i++)
|
||||||
{
|
{
|
||||||
await Task.Delay(100);
|
await Task.Delay(resetInterval);
|
||||||
monitor.OnValueReceived();
|
monitor.OnValueReceived();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should not have gone stale
|
// Should not have gone stale during the reset loop.
|
||||||
Assert.Equal(0, staleCount);
|
Assert.Equal(0, staleCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,12 +101,12 @@ public class StaleTagMonitorTests
|
|||||||
monitor.Stale += () => Interlocked.Increment(ref staleCount);
|
monitor.Stale += () => Interlocked.Increment(ref staleCount);
|
||||||
monitor.Start();
|
monitor.Start();
|
||||||
|
|
||||||
// Reset once
|
// Reset once before stale fires, then go silent and poll for the eventual fire.
|
||||||
await Task.Delay(50);
|
await Task.Delay(50);
|
||||||
monitor.OnValueReceived();
|
monitor.OnValueReceived();
|
||||||
|
|
||||||
// Then go silent
|
var fired = await PollUntilAsync(() => staleCount >= 1, PollTimeout);
|
||||||
await Task.Delay(250);
|
Assert.True(fired, "Stale event did not fire after silence following a reset.");
|
||||||
Assert.Equal(1, staleCount);
|
Assert.Equal(1, staleCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,13 +118,15 @@ public class StaleTagMonitorTests
|
|||||||
monitor.Stale += () => Interlocked.Increment(ref staleCount);
|
monitor.Stale += () => Interlocked.Increment(ref staleCount);
|
||||||
monitor.Start();
|
monitor.Start();
|
||||||
|
|
||||||
// Wait for first stale
|
// Wait for first stale via poll.
|
||||||
await Task.Delay(250);
|
var firstFired = await PollUntilAsync(() => staleCount >= 1, PollTimeout);
|
||||||
|
Assert.True(firstFired, "First Stale event did not fire.");
|
||||||
Assert.Equal(1, staleCount);
|
Assert.Equal(1, staleCount);
|
||||||
|
|
||||||
// Reset — should allow second stale fire
|
// Reset — should allow second stale fire.
|
||||||
monitor.OnValueReceived();
|
monitor.OnValueReceived();
|
||||||
await Task.Delay(250);
|
var secondFired = await PollUntilAsync(() => staleCount >= 2, PollTimeout);
|
||||||
|
Assert.True(secondFired, "Second Stale event did not fire after reset.");
|
||||||
Assert.Equal(2, staleCount);
|
Assert.Equal(2, staleCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user