test(otopcua): negative + convergence coverage for rebind re-trigger (follow-up C)
This commit is contained in:
+74
-1
@@ -45,6 +45,7 @@ public sealed class DriverHostActorDiscoveryTests : RuntimeActorTestBase
|
||||
private static readonly NodeId TestNode = NodeId.Parse("driver-disc-test");
|
||||
private static readonly RevisionHash RevA = RevisionHash.Parse(new string('a', 64));
|
||||
private static readonly RevisionHash RevB = RevisionHash.Parse(new string('b', 64));
|
||||
private static readonly RevisionHash RevC = RevisionHash.Parse(new string('c', 64));
|
||||
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(5);
|
||||
private static readonly DateTime Ts = new(2026, 6, 26, 10, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
@@ -517,9 +518,81 @@ public sealed class DriverHostActorDiscoveryTests : RuntimeActorTestBase
|
||||
AwaitAssert(() => factory.DiscoverCount.ShouldBeGreaterThan(1), duration: TimeSpan.FromSeconds(10));
|
||||
|
||||
// ... and the re-triggered pass re-grafts the FixedTree under the NEW equipment EQ-2 (the host resolves
|
||||
// d1 → EQ-2 against the new _lastComposition).
|
||||
// d1 → EQ-2 against the new _lastComposition). The re-discovery re-populates the cache as
|
||||
// { EQ-2: plan-scoped-to-EQ-2 }.
|
||||
publish.FishForMessage<OpcUaPublishActor.MaterialiseDiscoveredNodes>(
|
||||
m => m.EquipmentRootNodeId == "EQ-2", Timeout);
|
||||
var discoverCountAfterRebind = factory.DiscoverCount; // 2 (connect pass + the rebind re-trigger pass)
|
||||
|
||||
// Convergence: a FURTHER unchanged redeploy (SAME EQ-2 composition) must NOT drop again, NOT re-trigger,
|
||||
// and NOT advance discovery — proving the re-trigger CONVERGES and doesn't loop. The cached EQ-2 plan now
|
||||
// SURVIVES the re-inject tail (candidate + NodeIds still scoped to EQ-2), so EQ-2 simply re-materialises
|
||||
// (the Task-8 survivor re-apply) — but droppedAny stays false, so no TriggerRediscovery is sent and no
|
||||
// fresh discovery pass runs.
|
||||
var deploymentId3 = SeedDeploymentWithEquipmentTags(db, RevC,
|
||||
(Equip: "EQ-2", Driver: "d1", FullName: "40001", Folder: (string?)null, Name: "speed"));
|
||||
actor.Tell(new DispatchDeployment(deploymentId3, RevC, CorrelationId.NewId()));
|
||||
coordinator.ExpectMsg<ApplyAck>(Timeout).Outcome.ShouldBe(ApplyAckOutcome.Applied);
|
||||
publish.ExpectMsg<OpcUaPublishActor.RebuildAddressSpace>(Timeout);
|
||||
// The survivor re-apply re-materialises EQ-2 (NOT a drop+re-trigger).
|
||||
publish.ExpectMsg<OpcUaPublishActor.MaterialiseDiscoveredNodes>(Timeout)
|
||||
.EquipmentRootNodeId.ShouldBe("EQ-2");
|
||||
// No further publish traffic and NO further discovery pass — the re-trigger converged.
|
||||
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
||||
factory.DiscoverCount.ShouldBe(discoverCountAfterRebind);
|
||||
}
|
||||
|
||||
/// <summary>Negative / regression guard: a routine redeploy of a discovery-capable driver that does NOT
|
||||
/// rebind (SAME composition, new revision) must NOT spuriously re-trigger discovery. The cached EQ-1 plan
|
||||
/// SURVIVES the re-inject tail (its equipment still resolves and its NodeIds are still EQ-1-scoped), so it is
|
||||
/// re-applied (EQ-1 re-materialises — the Task-8 survival path) — but NOTHING is dropped, so <c>droppedAny</c>
|
||||
/// stays false and no <see cref="DriverInstanceActor.TriggerRediscovery"/> is sent. Uses the REAL
|
||||
/// <see cref="ITagDiscovery"/> child (the existing <c>ExpectNoMsg</c> drop tests use a NON-discovery driver,
|
||||
/// whose child would no-op a <c>TriggerRediscovery</c> in <c>StartDiscovery</c>'s <c>is not ITagDiscovery</c>
|
||||
/// guard — so they would NOT catch a future regression that sets <c>droppedAny</c> on a non-drop path). The
|
||||
/// regression is observable here: a spurious trigger would advance <c>DiscoverCount</c> past the single Once
|
||||
/// pass.</summary>
|
||||
[Fact]
|
||||
public void No_drop_redeploy_does_not_re_trigger_discovery()
|
||||
{
|
||||
var db = NewInMemoryDbFactory();
|
||||
var factory = new DiscoverableSubscribingDriverFactory("Modbus");
|
||||
var deploymentId = SeedDeploymentWithEquipmentTags(db, RevA,
|
||||
(Equip: "EQ-1", Driver: "d1", FullName: "40001", Folder: (string?)null, Name: "speed"));
|
||||
|
||||
var coordinator = CreateTestProbe();
|
||||
var publish = CreateTestProbe();
|
||||
var vtHost = CreateTestProbe();
|
||||
|
||||
var actor = Sys.ActorOf(DriverHostActor.Props(
|
||||
db, TestNode, coordinator.Ref,
|
||||
driverFactory: factory,
|
||||
localRoles: new HashSet<string> { "driver" },
|
||||
opcUaPublishActor: publish.Ref,
|
||||
virtualTagHostOverride: vtHost.Ref));
|
||||
|
||||
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
|
||||
coordinator.ExpectMsg<ApplyAck>(Timeout).Outcome.ShouldBe(ApplyAckOutcome.Applied);
|
||||
publish.ExpectMsg<OpcUaPublishActor.RebuildAddressSpace>(Timeout);
|
||||
|
||||
// Connect-time discovery grafts under EQ-1 and populates the cache (DiscoverCount == 1).
|
||||
publish.FishForMessage<OpcUaPublishActor.MaterialiseDiscoveredNodes>(
|
||||
m => m.EquipmentRootNodeId == "EQ-1", Timeout);
|
||||
AwaitAssert(() => factory.DiscoverCount.ShouldBe(1), duration: Timeout);
|
||||
|
||||
// Redeploy with the SAME composition (new revision so it applies; NO rebind ⇒ NO drop).
|
||||
var deploymentId2 = SeedDeploymentWithEquipmentTags(db, RevB,
|
||||
(Equip: "EQ-1", Driver: "d1", FullName: "40001", Folder: (string?)null, Name: "speed"));
|
||||
actor.Tell(new DispatchDeployment(deploymentId2, RevB, CorrelationId.NewId()));
|
||||
coordinator.ExpectMsg<ApplyAck>(Timeout).Outcome.ShouldBe(ApplyAckOutcome.Applied);
|
||||
publish.ExpectMsg<OpcUaPublishActor.RebuildAddressSpace>(Timeout);
|
||||
// The cached EQ-1 plan SURVIVES the re-inject tail and is re-applied — EQ-1 re-materialises.
|
||||
publish.ExpectMsg<OpcUaPublishActor.MaterialiseDiscoveredNodes>(Timeout)
|
||||
.EquipmentRootNodeId.ShouldBe("EQ-1");
|
||||
// But NOTHING was dropped, so NO TriggerRediscovery was sent: no fresh discovery pass runs and no
|
||||
// further publish traffic arrives. DiscoverCount stays 1.
|
||||
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
||||
factory.DiscoverCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>Spawns the host with the subscribing driver factory + a publish probe, dispatches the
|
||||
|
||||
Reference in New Issue
Block a user