test(otopcua): tighten multi-device collapse assertion + clear warn-state on removal (follow-up E)

This commit is contained in:
Joseph Doherty
2026-06-26 15:16:59 -04:00
parent 50f08635ec
commit 0074f37a64
2 changed files with 61 additions and 3 deletions
@@ -233,10 +233,15 @@ public sealed class DriverHostActorDiscoveryTests : RuntimeActorTestBase
byEquipment["EQ-B"].Variables.ShouldHaveSingleItem().DisplayName.ShouldBe("Run");
var eqANodeId = byEquipment["EQ-A"].Variables[0].NodeId;
var eqBNodeId = byEquipment["EQ-B"].Variables[0].NodeId;
// Single device per partition ⇒ the mapper collapses the host folder ⇒ the NodeId carries no host.
eqANodeId.ShouldStartWith("EQ-A/");
// Single device per partition ⇒ the mapper collapses the host folder ⇒ the NodeId carries NO host
// segment and reads EQ-n/FOCAS/<leaf>/<name>. Assert the EXACT collapsed path (depth + leaf) so a
// collapse regression — which would re-introduce the host folder (e.g. EQ-A/FOCAS/H1/Identity/Model) —
// fails here. Belt-and-suspenders: the raw discovered "H1" segment is also checked case-SENSITIVELY
// (a lowercase-only "h1" check would miss a leaked raw "H1").
eqANodeId.ShouldBe(EquipmentNodeIds.Variable("EQ-A", "FOCAS/Identity", "Model"));
eqANodeId.ShouldNotContain("H1");
eqANodeId.ShouldNotContain("h1");
eqBNodeId.ShouldStartWith("EQ-B/");
eqBNodeId.ShouldBe(EquipmentNodeIds.Variable("EQ-B", "FOCAS/Status", "Run"));
eqBNodeId.ShouldNotContain("h2");
// (b) The driver subscribes the UNION of both devices' FixedTree refs (tag-less ⇒ no authored refs).
@@ -294,6 +299,50 @@ public sealed class DriverHostActorDiscoveryTests : RuntimeActorTestBase
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
}
/// <summary>Warn-spam taming (follow-up E, part 2): an unmatched device-host partition WARNS exactly ONCE,
/// then the identical repeated re-discovery passes (the driver re-discovers ~15×/connect, re-sending the
/// same set) are quiet — proving <c>ShouldWarnPartition</c>'s per-driver signature dedup. The repeat is
/// logged at Debug, which the suite's <c>loglevel = WARNING</c> HOCON suppresses at source, so EventFilter
/// observes the dedup as "zero further matching warnings". (The matched EQ-A/EQ-B partitions still graft on
/// pass 1; pass 2's matched routing is short-circuited by <c>PlansRoutingEqual</c>.)</summary>
[Fact]
public void Repeated_unmatched_device_host_partition_warns_once_then_is_quiet()
{
var db = NewInMemoryDbFactory();
var factory = new SubscribingDriverFactory("Modbus");
var deploymentId = SeedDeploymentWithMultiDeviceEquipments(db, RevA, driverId: "d1",
(Equip: "EQ-A", DeviceId: "dev-a", Host: "h1"),
(Equip: "EQ-B", DeviceId: "dev-b", Host: "h2"));
var (actor, publish, _) = SpawnHostAndApply(db, deploymentId, factory);
var discovered = new[]
{
new DiscoveredNode(FolderPathSegments: new[] { "FOCAS", "h1", "Identity" },
BrowseName: "Model", DisplayName: "Model", FullReference: "ft-h1-1",
DataType: DriverDataType.Float64, IsArray: false, ArrayDim: null, Writable: false, IsHistorized: false),
new DiscoveredNode(FolderPathSegments: new[] { "FOCAS", "h2", "Status" },
BrowseName: "Run", DisplayName: "Run", FullReference: "ft-h2-1",
DataType: DriverDataType.Float64, IsArray: false, ArrayDim: null, Writable: false, IsHistorized: false),
new DiscoveredNode(FolderPathSegments: new[] { "FOCAS", "h3", "Identity" },
BrowseName: "Ghost", DisplayName: "Ghost", FullReference: "ft-h3-1",
DataType: DriverDataType.Float64, IsArray: false, ArrayDim: null, Writable: false, IsHistorized: false),
};
// Pass 1: the unmatched "h3" partition warns EXACTLY once.
EventFilter.Warning(contains: "discovered device-host partition(s) skipped")
.Expect(1, () => actor.Tell(new DriverInstanceActor.DiscoveredNodesReady("d1", discovered)));
// Drain the matched EQ-A + EQ-B grafts from pass 1 so the assertion below is unambiguous.
publish.ExpectMsg<OpcUaPublishActor.MaterialiseDiscoveredNodes>(Timeout);
publish.ExpectMsg<OpcUaPublishActor.MaterialiseDiscoveredNodes>(Timeout);
// Pass 2: the IDENTICAL set (same revision) does NOT warn again — the repeat is Debug (suppressed by the
// suite's WARNING loglevel), so EventFilter sees ZERO further matching warnings.
EventFilter.Warning(contains: "discovered device-host partition(s) skipped")
.Expect(0, () => actor.Tell(new DriverInstanceActor.DiscoveredNodesReady("d1", discovered)));
}
/// <summary>Guard: a <see cref="DriverInstanceActor.DiscoveredNodesReady"/> arriving BEFORE any deployment
/// is applied (<c>_lastComposition</c> still null) is ignored — nothing is materialised on the publish
/// side (the equipment can't be resolved without a composition).</summary>