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:
+130
-57
@@ -11,14 +11,14 @@ using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
|
||||
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// WP-6: Tests for DataConnectionActor Become/Stash state machine.
|
||||
/// WP-9: Auto-reconnect and bad quality tests.
|
||||
/// WP-10: Transparent re-subscribe tests.
|
||||
/// WP-11: Write-back support tests.
|
||||
/// WP-12: Tag path resolution with retry tests.
|
||||
/// WP-13: Health reporting tests.
|
||||
/// WP-14: Subscription lifecycle tests.
|
||||
/// Task-4: Failover state machine tests.
|
||||
/// Tests for DataConnectionActor Become/Stash state machine.
|
||||
/// Auto-reconnect and bad quality tests.
|
||||
/// Transparent re-subscribe tests.
|
||||
/// Write-back support tests.
|
||||
/// Tag path resolution with retry tests.
|
||||
/// Health reporting tests.
|
||||
/// Subscription lifecycle tests.
|
||||
/// Failover state machine tests.
|
||||
/// </summary>
|
||||
public class DataConnectionActorTests : TestKit
|
||||
{
|
||||
@@ -27,6 +27,7 @@ public class DataConnectionActorTests : TestKit
|
||||
private readonly ISiteHealthCollector _mockHealthCollector;
|
||||
private readonly IDataConnectionFactory _mockFactory;
|
||||
|
||||
/// <summary>Initializes a new instance of <c>DataConnectionActorTests</c>.</summary>
|
||||
public DataConnectionActorTests()
|
||||
: base(@"akka.loglevel = DEBUG")
|
||||
{
|
||||
@@ -39,7 +40,7 @@ public class DataConnectionActorTests : TestKit
|
||||
TagResolutionRetryInterval = TimeSpan.FromMilliseconds(200),
|
||||
WriteTimeout = TimeSpan.FromSeconds(5),
|
||||
// Default to a single seed-read attempt so existing tests behave exactly as
|
||||
// before the DCL-027 retry was added; the retry-specific tests opt in.
|
||||
// before the seed-read retry was added; the retry-specific tests opt in.
|
||||
SeedReadMaxAttempts = 1,
|
||||
SeedReadRetryDelay = TimeSpan.FromMilliseconds(10)
|
||||
};
|
||||
@@ -93,6 +94,7 @@ public class DataConnectionActorTests : TestKit
|
||||
c.GetArguments() is [string name, ConnectionHealth.Connected, ..] &&
|
||||
name == connectionName);
|
||||
|
||||
/// <summary>Verifies that starts in connecting state attempts connect.</summary>
|
||||
[Fact]
|
||||
public void WP6_StartsInConnectingState_AttemptsConnect()
|
||||
{
|
||||
@@ -107,6 +109,7 @@ public class DataConnectionActorTests : TestKit
|
||||
TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that connecting state stashes subscribe requests.</summary>
|
||||
[Fact]
|
||||
public void WP6_ConnectingState_StashesSubscribeRequests()
|
||||
{
|
||||
@@ -134,6 +137,8 @@ public class DataConnectionActorTests : TestKit
|
||||
ExpectMsg<SubscribeTagsResponse>(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that connected state write returns result.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WP11_ConnectedState_Write_ReturnsResult()
|
||||
{
|
||||
@@ -158,6 +163,8 @@ public class DataConnectionActorTests : TestKit
|
||||
Assert.True(response.Success);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that write failure returned synchronously.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WP11_Write_Failure_ReturnedSynchronously()
|
||||
{
|
||||
@@ -177,6 +184,8 @@ public class DataConnectionActorTests : TestKit
|
||||
Assert.Equal("Device offline", response.ErrorMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that health report returns connection status.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WP13_HealthReport_ReturnsConnectionStatus()
|
||||
{
|
||||
@@ -195,8 +204,10 @@ public class DataConnectionActorTests : TestKit
|
||||
Assert.Equal(ConnectionHealth.Connected, report.Status);
|
||||
}
|
||||
|
||||
// ── Task-4: Failover state machine tests ──
|
||||
// ── Failover state machine tests ──
|
||||
|
||||
/// <summary>Verifies that failover after n retries switches to backup.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Task4_FailoverAfterNRetries_SwitchesToBackup()
|
||||
{
|
||||
@@ -242,6 +253,8 @@ public class DataConnectionActorTests : TestKit
|
||||
TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that single endpoint retries indefinitely no failover.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Task4_SingleEndpoint_RetriesIndefinitely_NoFailover()
|
||||
{
|
||||
@@ -275,6 +288,8 @@ public class DataConnectionActorTests : TestKit
|
||||
_mockFactory.DidNotReceive().Create(Arg.Any<string>(), Arg.Any<IDictionary<string, string>>());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that round robin back to primary after backup fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Task4_RoundRobin_BackToPrimary_AfterBackupFails()
|
||||
{
|
||||
@@ -352,6 +367,8 @@ public class DataConnectionActorTests : TestKit
|
||||
TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that successful reconnect resets failure counter.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Task4_SuccessfulReconnect_ResetsFailureCounter()
|
||||
{
|
||||
@@ -406,6 +423,8 @@ public class DataConnectionActorTests : TestKit
|
||||
_mockFactory.DidNotReceive().Create(Arg.Any<string>(), Arg.Any<IDictionary<string, string>>());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that re subscribe all called after failover reconnect.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Task4_ReSubscribeAll_CalledAfterFailoverReconnect()
|
||||
{
|
||||
@@ -485,8 +504,10 @@ public class DataConnectionActorTests : TestKit
|
||||
"sensor/temp", Arg.Any<SubscriptionCallback>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
// ── PLAN-03 Task 16 (S7): background tasks must use the adapter that started them ──
|
||||
// ── Background tasks must use the adapter that started them ──
|
||||
|
||||
/// <summary>Verifies that in flight subscribe continues on original adapter after failover.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Task16_InFlightSubscribe_ContinuesOnOriginalAdapter_AfterFailover()
|
||||
{
|
||||
@@ -545,7 +566,7 @@ public class DataConnectionActorTests : TestKit
|
||||
backupAdapter.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ConnectAsync"),
|
||||
TimeSpan.FromSeconds(5));
|
||||
|
||||
// Demand is registered before I/O (DC-1), so failover independently recovers
|
||||
// Demand is registered before I/O, so failover independently recovers
|
||||
// both tags on backup even while the original primary operation is parked.
|
||||
AwaitCondition(() => backupAdapter.ReceivedCalls().Count(c => c.GetMethodInfo().Name == "SubscribeAsync") == 2,
|
||||
TimeSpan.FromSeconds(5));
|
||||
@@ -562,7 +583,7 @@ public class DataConnectionActorTests : TestKit
|
||||
Assert.Equal(2, backupAdapter.ReceivedCalls().Count(c => c.GetMethodInfo().Name == "SubscribeAsync"));
|
||||
}
|
||||
|
||||
// ── PLAN-03 Task 17 (P3): tag→instances reverse index for the value fan-out hot path ──
|
||||
// ── Tag→instances reverse index for the value fan-out hot path ──
|
||||
|
||||
private IActorRef CreateInstantSubscribeActor(string name)
|
||||
{
|
||||
@@ -578,6 +599,8 @@ public class DataConnectionActorTests : TestKit
|
||||
return CreateConnectionActor(name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that tag fan out after one instance unsubscribes only remaining subscriber receives.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Task17_TagFanOut_AfterOneInstanceUnsubscribes_OnlyRemainingSubscriberReceives()
|
||||
{
|
||||
@@ -608,6 +631,8 @@ public class DataConnectionActorTests : TestKit
|
||||
probeA.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that tag fan out two instances disjoint tags each receives only its own.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Task17_TagFanOut_TwoInstancesDisjointTags_EachReceivesOnlyItsOwn()
|
||||
{
|
||||
@@ -631,7 +656,7 @@ public class DataConnectionActorTests : TestKit
|
||||
probeB.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
||||
}
|
||||
|
||||
// ── DataConnectionLayer-001: subscribe must not mutate actor state off-thread ──
|
||||
// ── Subscribe must not mutate actor state off-thread ──
|
||||
|
||||
private static async Task<string> DelayedSubscribeAsync()
|
||||
{
|
||||
@@ -641,10 +666,12 @@ public class DataConnectionActorTests : TestKit
|
||||
return "sub-" + Guid.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that concurrent subscribes do not corrupt subscription counters.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL001_ConcurrentSubscribes_DoNotCorruptSubscriptionCounters()
|
||||
{
|
||||
// Regression test for DataConnectionLayer-001. HandleSubscribe used to mutate
|
||||
// Regression test. HandleSubscribe used to mutate
|
||||
// actor state (_subscriptionIds, _totalSubscribed, _resolvedTags, the per-instance
|
||||
// HashSet) from a Task.Run background thread. Many concurrent subscribes then race
|
||||
// on non-thread-safe Dictionary/HashSet and on non-atomic int++ — losing increments
|
||||
@@ -684,12 +711,14 @@ public class DataConnectionActorTests : TestKit
|
||||
Assert.Equal(instances * tagsPerInstance, report.ResolvedTags);
|
||||
}
|
||||
|
||||
// ── DataConnectionLayer-004: subscribe-time failure classification ──
|
||||
// ── Subscribe-time failure classification ──
|
||||
|
||||
/// <summary>Verifies that genuine tag resolution failure pushes bad quality to subscriber.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL004_GenuineTagResolutionFailure_PushesBadQualityToSubscriber()
|
||||
{
|
||||
// Regression test for DataConnectionLayer-004. When a tag genuinely fails to
|
||||
// Regression test. When a tag genuinely fails to
|
||||
// resolve at subscribe time, the design doc (Tag Path Resolution, step 2)
|
||||
// requires the attribute to be marked quality `bad`. The pre-fix code only
|
||||
// logged and added the tag to _unresolvedTags — the Instance Actor never got
|
||||
@@ -718,10 +747,12 @@ public class DataConnectionActorTests : TestKit
|
||||
Assert.True(ack.Success);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that static tag seed value is delivered after registration.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL026_StaticTagSeedValue_IsDeliveredAfterRegistration()
|
||||
{
|
||||
// Regression test for DataConnectionLayer-026. The initial-read seed value used to
|
||||
// Regression test. The initial-read seed value used to
|
||||
// be emitted (TagValueReceived) from HandleSubscribe's background task BEFORE
|
||||
// HandleSubscribeCompleted registered the instance's tags in
|
||||
// _subscriptionsByInstance. HandleTagValueReceived's fan-out then found no
|
||||
@@ -755,8 +786,10 @@ public class DataConnectionActorTests : TestKit
|
||||
Assert.Equal("Left54321", update.Value);
|
||||
}
|
||||
|
||||
// ── DataConnectionLayer-027: robust seeding for STATIC tags ──
|
||||
// ── Robust seeding for STATIC tags ──
|
||||
|
||||
/// <summary>Verifies that seed read empty then good seeds tag within retry budget.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL027_SeedRead_EmptyThenGood_SeedsTagWithinRetryBudget()
|
||||
{
|
||||
@@ -792,6 +825,8 @@ public class DataConnectionActorTests : TestKit
|
||||
Assert.Equal("HY12333", update.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that seed read never returns value logs warning and still acks.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL027_SeedRead_NeverReturnsValue_LogsWarningAndStillAcks()
|
||||
{
|
||||
@@ -823,10 +858,12 @@ public class DataConnectionActorTests : TestKit
|
||||
Assert.True(ack.Success);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reconnect re seeds static tag.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL027_Reconnect_ReSeedsStaticTag()
|
||||
{
|
||||
// Mechanism #3: ReSubscribeAll re-advises after a reconnect but pre-fix did NO
|
||||
// Mechanism three: ReSubscribeAll re-advises after a reconnect but pre-fix did NO
|
||||
// seed read, so a STATIC tag (no further OnDataChange) could not self-heal across
|
||||
// a reconnect/failover even though the source reads Good. After the fix the
|
||||
// reconnect re-seeds the re-advised tags, so the post-reconnect value reaches the
|
||||
@@ -873,10 +910,12 @@ public class DataConnectionActorTests : TestKit
|
||||
Assert.Equal(99.0, reseeded.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that connection level subscribe failure triggers reconnect not tag retry.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL004_ConnectionLevelSubscribeFailure_TriggersReconnect_NotTagRetry()
|
||||
{
|
||||
// Regression test for DataConnectionLayer-004. A subscribe failing because the
|
||||
// Regression test. A subscribe failing because the
|
||||
// adapter is not connected (InvalidOperationException from EnsureConnected) is
|
||||
// a connection problem, not a bad tag path. The pre-fix code misclassified it
|
||||
// as an unresolved tag and retried it on the 10s tag-resolution timer. After
|
||||
@@ -904,12 +943,14 @@ public class DataConnectionActorTests : TestKit
|
||||
TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
// ── DataConnectionLayer-005: WriteTimeout must bound a hung write ──
|
||||
// ── WriteTimeout must bound a hung write ──
|
||||
|
||||
/// <summary>Verifies that write that hangs times out and returns failure synchronously.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL005_Write_ThatHangs_TimesOutAndReturnsFailureSynchronously()
|
||||
{
|
||||
// Regression test for DataConnectionLayer-005. HandleWrite called WriteAsync
|
||||
// Regression test. HandleWrite called WriteAsync
|
||||
// with no CancellationToken and no timeout, so a hung device write never
|
||||
// produced a WriteTagResponse. The calling script would block until its own
|
||||
// Ask-timeout with no DCL-level error. After the fix, _options.WriteTimeout
|
||||
@@ -939,12 +980,14 @@ public class DataConnectionActorTests : TestKit
|
||||
Assert.Contains("timeout", response.ErrorMessage, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// ── DataConnectionLayer-006: quality counters must not drift after unsubscribe/reconnect ──
|
||||
// ── Quality counters must not drift after unsubscribe/reconnect ──
|
||||
|
||||
/// <summary>Verifies that disconnect after unsubscribe bad quality count matches remaining tags.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL006_DisconnectAfterUnsubscribe_BadQualityCountMatchesRemainingTags()
|
||||
{
|
||||
// Regression test for DataConnectionLayer-006. _lastTagQuality and the three
|
||||
// Regression test. _lastTagQuality and the three
|
||||
// quality counters were never cleaned up on unsubscribe, so a tag removed via
|
||||
// HandleUnsubscribe lingered in _lastTagQuality. PushBadQualityForAllTags then
|
||||
// set _tagsBadQuality = _lastTagQuality.Count, counting the dropped tag and
|
||||
@@ -1008,12 +1051,14 @@ public class DataConnectionActorTests : TestKit
|
||||
reconnectGate.SetCanceled();
|
||||
}
|
||||
|
||||
// ── DataConnectionLayer-010: tag-resolution retry must not double-dispatch ──
|
||||
// ── Tag-resolution retry must not double-dispatch ──
|
||||
|
||||
/// <summary>Verifies that tag resolution retry does not issue duplicate concurrent subscribes.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL010_TagResolutionRetry_DoesNotIssueDuplicateConcurrentSubscribes()
|
||||
{
|
||||
// Regression test for DataConnectionLayer-010. HandleRetryTagResolution fired a
|
||||
// Regression test. HandleRetryTagResolution fired a
|
||||
// SubscribeAsync for every unresolved tag without removing it from _unresolvedTags
|
||||
// first. A slow SubscribeAsync overlapping the next retry tick produced duplicate
|
||||
// concurrent subscribe attempts for the same tag, leaking the first monitored
|
||||
@@ -1057,12 +1102,14 @@ public class DataConnectionActorTests : TestKit
|
||||
subscribeGate.SetCanceled();
|
||||
}
|
||||
|
||||
// ── DataConnectionLayer-018: concurrent same-tag subscribes must not orphan adapter handles ──
|
||||
// ── Concurrent same-tag subscribes must not orphan adapter handles ──
|
||||
|
||||
/// <summary>Verifies that concurrent subscribes same tag different instances issue one adapter subscribe.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL018_ConcurrentSubscribes_SameTag_DifferentInstances_IssueOneAdapterSubscribe()
|
||||
{
|
||||
// Regression test for DataConnectionLayer-018. Before the fix, HandleSubscribe
|
||||
// Regression test. Before the fix, HandleSubscribe
|
||||
// snapshotted _subscriptionIds.Keys on the actor thread BEFORE the Task.Run
|
||||
// I/O. Two SubscribeTagsRequest messages for different instances sharing a tag
|
||||
// would both observe "not subscribed" (the first request's SubscribeCompleted
|
||||
@@ -1130,17 +1177,19 @@ public class DataConnectionActorTests : TestKit
|
||||
actor.Tell(new DataConnectionActor.GetHealthReport());
|
||||
var report = ExpectMsg<DataConnectionHealthReport>(TimeSpan.FromSeconds(5));
|
||||
// One tag shared across two instances must count as exactly one subscription.
|
||||
// DCL-020 (#28) tracks the related "previously-unresolved tag promoted via
|
||||
// A separate scenario covers the related "previously-unresolved tag promoted via
|
||||
// another instance" double-count case; here the tag was never unresolved.
|
||||
Assert.Equal(1, report.ResolvedTags);
|
||||
}
|
||||
|
||||
// ── DataConnectionLayer-020: previously-unresolved tag, resolved via different instance, must not double-count ──
|
||||
// ── Previously-unresolved tag, resolved via different instance, must not double-count ──
|
||||
|
||||
/// <summary>Verifies that unresolved tag promoted by different instance does not double count total subscribed.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL020_UnresolvedTagPromoted_ByDifferentInstance_DoesNotDoubleCountTotalSubscribed()
|
||||
{
|
||||
// Regression test for DataConnectionLayer-020. The first SubscribeTagsRequest
|
||||
// Regression test. The first SubscribeTagsRequest
|
||||
// (instance A, tag "promote/tag") fails at the resolution layer — the tag is
|
||||
// added to _unresolvedTags AND _totalSubscribed is bumped to 1. The second
|
||||
// SubscribeTagsRequest (instance B, same tag) succeeds the adapter call. Before
|
||||
@@ -1185,10 +1234,12 @@ public class DataConnectionActorTests : TestKit
|
||||
Assert.Equal(1, report.ResolvedTags);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that two instances failing same tag only counts tag once in total.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL020_TwoInstancesFailingSameTag_OnlyCountsTagOnceInTotal()
|
||||
{
|
||||
// Regression test for DataConnectionLayer-020's symmetric failure branch.
|
||||
// Regression test for the symmetric failure branch of the same scenario.
|
||||
// Two instances both fail to resolve the same tag — _unresolvedTags must hold
|
||||
// a single entry and _totalSubscribed must be 1, not 2. Pre-fix the failure
|
||||
// branch always ran _totalSubscribed++, double-counting on the second
|
||||
@@ -1221,12 +1272,14 @@ public class DataConnectionActorTests : TestKit
|
||||
Assert.Equal(0, report.ResolvedTags);
|
||||
}
|
||||
|
||||
// ── DataConnectionLayer-021: mid-flight unsubscribe must release adapter handle and drop state ──
|
||||
// ── Mid-flight unsubscribe must release adapter handle and drop state ──
|
||||
|
||||
/// <summary>Verifies that unsubscribe during in flight subscribe releases adapter handle and keeps state clean.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL021_UnsubscribeDuringInFlightSubscribe_ReleasesAdapterHandle_AndKeepsStateClean()
|
||||
{
|
||||
// Regression test for DataConnectionLayer-021. Previously HandleSubscribeCompleted
|
||||
// Regression test. Previously HandleSubscribeCompleted
|
||||
// re-created _subscriptionsByInstance[instanceName] when the instance had been
|
||||
// unsubscribed while the subscribe I/O was in flight, and then ran the same
|
||||
// counter/handle mutations as the happy path. The leak permanently inflated
|
||||
@@ -1280,12 +1333,14 @@ public class DataConnectionActorTests : TestKit
|
||||
Assert.Equal(0, report.ResolvedTags);
|
||||
}
|
||||
|
||||
// ── DataConnectionLayer-022: tag-resolution retry timer must not reset on every failure ──
|
||||
// ── Tag-resolution retry timer must not reset on every failure ──
|
||||
|
||||
/// <summary>Verifies that bursted failed subscribes do not reset retry timer.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL022_BurstedFailedSubscribes_DoNotResetRetryTimer()
|
||||
{
|
||||
// Regression test for DataConnectionLayer-022. Both HandleSubscribeCompleted
|
||||
// Regression test. Both HandleSubscribeCompleted
|
||||
// and HandleTagResolutionFailed previously called Timers.StartPeriodicTimer
|
||||
// unconditionally — StartPeriodicTimer with an existing key cancels and
|
||||
// replaces the prior timer, so a burst of SubscribeTagsRequests arriving
|
||||
@@ -1357,12 +1412,14 @@ public class DataConnectionActorTests : TestKit
|
||||
$"This is the DCL-022 reset-on-every-call starvation regression.");
|
||||
}
|
||||
|
||||
// ── DataConnectionLayer-011: stale callbacks from a disposed adapter must be dropped ──
|
||||
// ── Stale callbacks from a disposed adapter must be dropped ──
|
||||
|
||||
/// <summary>Verifies that stale tag value from old adapter is not forwarded after failover.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL011_StaleTagValueFromOldAdapter_IsNotForwardedAfterFailover()
|
||||
{
|
||||
// Regression test for DataConnectionLayer-011. On failover the old adapter is
|
||||
// Regression test. On failover the old adapter is
|
||||
// disposed and a fresh one created, but the old adapter's subscription callbacks
|
||||
// captured Self and keep Telling TagValueReceived. With no per-adapter generation
|
||||
// tag, a value from the disposed adapter delivered after the actor is Connected
|
||||
@@ -1430,12 +1487,14 @@ public class DataConnectionActorTests : TestKit
|
||||
ExpectNoMsg(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
// ── DataConnectionLayer-015: initial-connect failures must trigger failover ──
|
||||
// ── Initial-connect failures must trigger failover ──
|
||||
|
||||
/// <summary>Verifies that primary down at startup fails over to backup.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL015_PrimaryDownAtStartup_FailsOverToBackup()
|
||||
{
|
||||
// Regression test for DataConnectionLayer-015. HandleConnectResult — the handler
|
||||
// Regression test. HandleConnectResult — the handler
|
||||
// for the INITIAL connection attempt in the Connecting state — only logged and
|
||||
// re-armed the reconnect timer. It never incremented _consecutiveFailures and
|
||||
// never switched endpoint, so a primary that is unreachable when the actor first
|
||||
@@ -1470,6 +1529,8 @@ public class DataConnectionActorTests : TestKit
|
||||
TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that single endpoint down at startup retries indefinitely no failover.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL015_SingleEndpointDownAtStartup_RetriesIndefinitely_NoFailover()
|
||||
{
|
||||
@@ -1495,12 +1556,14 @@ public class DataConnectionActorTests : TestKit
|
||||
_mockFactory.DidNotReceive().Create(Arg.Any<string>(), Arg.Any<IDictionary<string, string>>());
|
||||
}
|
||||
|
||||
// ── DataConnectionLayer-016: subscribe response must reflect a connection-level failure ──
|
||||
// ── Subscribe response must reflect a connection-level failure ──
|
||||
|
||||
/// <summary>Verifies that connection level subscribe failure replies with unsuccessful response.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL016_ConnectionLevelSubscribeFailure_RepliesWithUnsuccessfulResponse()
|
||||
{
|
||||
// Regression test for DataConnectionLayer-016. When a subscribe arrives while the
|
||||
// Regression test. When a subscribe arrives while the
|
||||
// adapter is silently down, HandleSubscribeCompleted drove the actor into
|
||||
// Reconnecting (a connection-level failure) but still replied to the caller with
|
||||
// SubscribeTagsResponse(Success: true, Error: null). The Instance Actor was told
|
||||
@@ -1530,6 +1593,8 @@ public class DataConnectionActorTests : TestKit
|
||||
Assert.NotNull(response.ErrorMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that genuine resolution failure still replies success.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL016_GenuineResolutionFailure_StillRepliesSuccess()
|
||||
{
|
||||
@@ -1556,6 +1621,8 @@ public class DataConnectionActorTests : TestKit
|
||||
Assert.Null(ack.ErrorMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that subscribe with failed tags counts resolved and unresolved separately.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL001_SubscribeWithFailedTags_CountsResolvedAndUnresolvedSeparately()
|
||||
{
|
||||
@@ -1579,7 +1646,7 @@ public class DataConnectionActorTests : TestKit
|
||||
["good/a", "good/b", "good/c", "bad/x", "bad/y"], DateTimeOffset.UtcNow));
|
||||
|
||||
// Two genuine resolution failures now also push a bad-quality TagValueUpdate
|
||||
// to the subscriber (DataConnectionLayer-004); skip past those to the ack.
|
||||
// to the subscriber; skip past those to the ack.
|
||||
var ack = FishForMessage<SubscribeTagsResponse>(_ => true, TimeSpan.FromSeconds(5));
|
||||
Assert.True(ack.Success);
|
||||
|
||||
@@ -1590,12 +1657,14 @@ public class DataConnectionActorTests : TestKit
|
||||
Assert.Equal(3, report.ResolvedTags); // only the 3 good ones resolved
|
||||
}
|
||||
|
||||
// ── DataConnectionLayer-008: HandleUnsubscribe shared-tag reference counting ──
|
||||
// ── HandleUnsubscribe shared-tag reference counting ──
|
||||
|
||||
/// <summary>Verifies that unsubscribe only releases tag when last subscriber leaves.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL008_Unsubscribe_OnlyReleasesTagWhenLastSubscriberLeaves()
|
||||
{
|
||||
// Regression test for DataConnectionLayer-008. HandleUnsubscribe must release a
|
||||
// Regression test. HandleUnsubscribe must release a
|
||||
// tag at the adapter only when no other instance still subscribes to it. The
|
||||
// O(n) per-tag scan over every instance was replaced with an O(1) reference
|
||||
// count; this guards that the reference count tracks shared subscriptions
|
||||
@@ -1649,14 +1718,16 @@ public class DataConnectionActorTests : TestKit
|
||||
Assert.Equal(0, report2.ResolvedTags);
|
||||
}
|
||||
|
||||
// ── DataConnectionLayer-027: a reconnect re-seed read that lands after a SECOND
|
||||
// ── A reconnect re-seed read that lands after a SECOND
|
||||
// failover (generation bumped twice) must be DROPPED, not applied ──
|
||||
|
||||
/// <summary>Verifies that stale generation reseed after second failover is dropped.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DCL027_StaleGenerationReseed_AfterSecondFailover_IsDropped()
|
||||
{
|
||||
// Regression/invariant pin for DataConnectionLayer-027 (the reconnect re-seed in
|
||||
// ReSubscribeAll). The re-seed captures the adapter generation at kickoff and
|
||||
// Regression/invariant pin for the reconnect re-seed in
|
||||
// ReSubscribeAll. The re-seed captures the adapter generation at kickoff and
|
||||
// delivers the seed value through the generation-guarded TagValueReceived path:
|
||||
//
|
||||
// var generation = _adapterGeneration; // captured at reseed kickoff
|
||||
@@ -1712,7 +1783,7 @@ public class DataConnectionActorTests : TestKit
|
||||
backupAdapter.ReadAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(_ =>
|
||||
{
|
||||
// Signal the test that the generation-1 re-seed read has been entered
|
||||
// Signal the test that the generation 1 re-seed read has been entered
|
||||
// (so generation 1 is definitely captured), then hand back the gated task.
|
||||
backupReadEntered.TrySetResult();
|
||||
return staleSeedReadGate.Task;
|
||||
@@ -1720,7 +1791,7 @@ public class DataConnectionActorTests : TestKit
|
||||
|
||||
// ── Primary-again (generation 2): the round-robin target of the SECOND failover.
|
||||
// Connects cleanly; its re-seed read returns a DISTINCT fresh value (77.0) so we
|
||||
// can prove the FRESH generation-2 value IS accepted while the stale gen-1 is not. ──
|
||||
// can prove the FRESH generation 2 value IS accepted while the stale gen 1 is not. ──
|
||||
primaryAgainAdapter.ConnectAsync(Arg.Any<IDictionary<string, string>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.CompletedTask);
|
||||
primaryAgainAdapter.Status.Returns(ConnectionHealth.Connected);
|
||||
@@ -1755,12 +1826,12 @@ public class DataConnectionActorTests : TestKit
|
||||
RaiseDisconnected(primaryAdapter);
|
||||
ExpectMsg<ConnectionQualityChanged>(TimeSpan.FromSeconds(3)); // bad-quality push on disconnect
|
||||
|
||||
// Wait until the generation-1 re-seed read has been ENTERED (generation 1 captured,
|
||||
// Wait until the generation 1 re-seed read has been ENTERED (generation 1 captured,
|
||||
// read pending on the gate). This anchors "generation N captured at reseed kickoff".
|
||||
// WaitAsync throws TimeoutException (failing the test) if the read is never entered.
|
||||
await backupReadEntered.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
|
||||
// ── SECOND failover (while the gen-1 re-seed read is still blocked): disconnect
|
||||
// ── SECOND failover (while the gen 1 re-seed read is still blocked): disconnect
|
||||
// backup → unstable → failover round-robin back to primary config (generation
|
||||
// bumps 1 → 2). After this the actor's current generation is 2. ──
|
||||
RaiseDisconnected(backupAdapter);
|
||||
@@ -1774,16 +1845,16 @@ public class DataConnectionActorTests : TestKit
|
||||
TimeSpan.FromSeconds(5));
|
||||
await Task.Delay(200); // actor Connected on primary-again (generation 2)
|
||||
|
||||
// The FRESH generation-2 re-seed (read 77.0 on primary-again) is the only value
|
||||
// The FRESH generation 2 re-seed (read 77.0 on primary-again) is the only value
|
||||
// that should be applied. It may arrive before or after we release the stale seed;
|
||||
// fish for it specifically and assert the stale value is never among the updates.
|
||||
// ── Now release the gated generation-1 backup re-seed read. The continuation
|
||||
// ── Now release the gated generation 1 backup re-seed read. The continuation
|
||||
// Tells TagValueReceived(generation = 1); the actor's generation is now 2, so
|
||||
// HandleTagValueReceived's guard MUST drop it. ──
|
||||
staleSeedReadGate.SetResult(
|
||||
new ReadResult(true, new TagValue(staleValue, QualityCode.Good, DateTimeOffset.UtcNow), null));
|
||||
|
||||
// The fresh generation-2 value (77.0) is accepted and reaches the subscriber;
|
||||
// The fresh generation 2 value (77.0) is accepted and reaches the subscriber;
|
||||
// FishForMessage will fail the test outright if it ever sees the stale 11.0.
|
||||
var fresh = FishForMessage<TagValueUpdate>(
|
||||
m =>
|
||||
@@ -1801,6 +1872,8 @@ public class DataConnectionActorTests : TestKit
|
||||
|
||||
// ── Deferred register row 55: first-subscribe resolution counts never reached the collector ──
|
||||
|
||||
/// <summary>Verifies that first subscribe all tags resolve pushes resolution counts to health collector.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task FirstSubscribe_AllTagsResolve_PushesResolutionCountsToHealthCollector()
|
||||
{
|
||||
@@ -1839,7 +1912,7 @@ public class DataConnectionActorTests : TestKit
|
||||
_mockHealthCollector.Received().UpdateTagResolution("row55-first-subscribe", 0, 0);
|
||||
|
||||
// One instance subscribes to two tags; both resolve on the first attempt, so the
|
||||
// batch/probe path never runs — exactly the rig's rig-probe-1 shape.
|
||||
// batch/probe path never runs — exactly the rig's rig probe 1 shape.
|
||||
actor.Tell(new SubscribeTagsRequest(
|
||||
"c1", "inst1", "row55-first-subscribe",
|
||||
["sensor/temp", "sensor/pressure"], DateTimeOffset.UtcNow));
|
||||
|
||||
Reference in New Issue
Block a user