test+docs: retire the stale EquipmentTagsDarkBatch4 skips, guard the deploy-time
tag gate, and fix three silently-broken OpcUaClient integration tests
Continues working through deferment.md's remaining items.
SKIPPED TESTS (13 -> 0 for this reason). The skip said "dark until Batch 4";
Batch 4 shipped as v3.0 and RETIRED the equipment-tag path rather than lighting
it, so every one of them was waiting on something that had already happened and
gone the other way. Resolved individually rather than in bulk:
- REVIVED onto the raw/UNS shape (6): four DriverHostActor primary-gate cases,
the cluster-scoping rebuild, and the real-SDK dual-namespace materialisation
E2E. The behaviour never went dark, only the v2 seeding did.
- FOLDED into a live parity test (3): DeploymentArtifactRawUnsParityTests
already compared RawTagPlan record-equal and RawTagPlan carries array /
historize / alarm intent, so widening its corpus by two tags covers what
three empty placeholder suites had been promising. Those suites are deleted.
- DELETED (4): subjects genuinely retired -- equipment-namespace EquipmentTags,
and both dot-joint {{equip}}.X token suites (that resolver was deleted in
Batch 3; the slash-joint form is covered by DeploymentArtifactEquipRefParityTests).
Falsified, not assumed: removing the seeded UnsTagReference turns two revived
primary-gate tests red; re-homing the SITE-A node to MAIN turns the scoping test
red. Widening a parity corpus also needed direct value assertions -- parity alone
cannot distinguish "both seams right" from "both seams blind".
Runtime.Tests skips 13->3, OpcUaServer.Tests 4->1; all remaining are deliberate
EquipmentDeviceBindingRetired tombstones.
G-7 (deploy-time TagConfig gate). MQTT shipped an Inspect() nobody ever called;
now wired. Replaced the hand-maintained list with a reflection guard, because the
existing test enumerates driver types by hand and so guards renames but can never
notice a gap. NOTE: the first version of that guard was hollow -- it discovered
candidates via GetReferencedAssemblies(), which is circular, since the compiler
elides a reference nothing in the IL uses. Removing MQTT from the map also removed
its Contracts assembly from the manifest, and the guard passed green with the bug
reintroduced. Rewritten to scan the output directory; re-falsified and it now
fails naming MqttTagDefinitionFactory. Residual recorded at the code: Sql,
MTConnect, Calculation and OpcUaClient have no Contracts-side Inspect at all, so
the deploy gate stays weaker than the AdminUI's authoring gate for them.
OPCUACLIENT INTEGRATION TESTS (3 of 4 failing, on master too). Not fixture rot:
the simulator was healthy and Client.CLI read the very same node Good. v3 made the
driver resolve every reference through its authored RawTags table, so a bare
"ns=3;s=StepUp" fails LOCALLY at the resolver with BadNodeIdInvalid and never
reaches the wire -- the tests were exercising the unresolved-reference guard.
Fixed by seeding OpcPlcProfile.RawTags in the deploy artifact's TagConfig shape
and addressing by RawPath. 4/4 pass.
DOCS. Applied the Phase7* -> AddressSpace* rename across the four live docs
(historical bannered files deliberately untouched), resolving the three that are
not renames to their real members. Found en route that the earlier banner pass
missed three files: VirtualTags.md, OpcUaServer.md and ScriptedAlarms.md all
still presented the test-scaffolding GenericDriverNodeManager as the production
dispatch path. All three now carry the correction.
Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
This commit is contained in:
+113
@@ -0,0 +1,113 @@
|
||||
using System.Reflection;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Guards the deploy-time TagConfig gate against the failure mode that produced deferment-register G-7:
|
||||
/// a driver ships an <c>Inspect</c> on its <c>*TagDefinitionFactory</c>, nobody adds it to
|
||||
/// <see cref="EquipmentTagConfigInspector"/>, and the gate is silently blind for that driver at deploy
|
||||
/// while the AdminUI still validates it at authoring time. MQTT sat in exactly that state.
|
||||
/// <para><b>Why reflection rather than a hand-written list.</b> The sibling
|
||||
/// <c>EquipmentTagConfigInspectorTests</c> enumerates driver-type strings by hand, so it guards
|
||||
/// <i>renames</i> but can never notice a <i>gap</i> — a newly-inspectable driver simply never appears in
|
||||
/// it. This is the same hollow-guard shape found in <c>TagConfigDriverTypeNameGuardTests</c> and
|
||||
/// <c>CsvColumnMapReflectionTests</c>. Deriving the expected set from the assemblies means the test
|
||||
/// fails the moment a new inspector exists, without anyone remembering to update it.</para>
|
||||
/// </summary>
|
||||
public sealed class EquipmentTagConfigInspectorCoverageTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Every <c>*TagDefinitionFactory</c> reachable from ControlPlane that exposes a public static
|
||||
/// <c>Inspect(string)</c> must be wired into the dispatch map.
|
||||
/// <para>Discovery walks the referenced assemblies rather than <c>AppDomain</c> alone, because a
|
||||
/// Contracts assembly with no other use is not loaded until first touched — an
|
||||
/// <c>AppDomain.GetAssemblies()</c> sweep would quietly find nothing and pass vacuously.</para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Every_driver_shipping_an_Inspect_is_wired_into_the_dispatch_map()
|
||||
{
|
||||
var inspectable = DiscoverInspectableFactories();
|
||||
|
||||
// Positive control: the discovery itself must find something, or the assertion below is vacuous —
|
||||
// an empty expected-set is satisfied by an empty map, which is the exact bug this guards.
|
||||
inspectable.Count.ShouldBeGreaterThan(
|
||||
1,
|
||||
"the reflection sweep found no (or one) driver factory with an Inspect — discovery is broken, "
|
||||
+ "and the coverage assertion below would pass no matter what the map contained");
|
||||
|
||||
var mapped = EquipmentTagConfigInspector.MappedDriverTypes;
|
||||
|
||||
var missing = inspectable
|
||||
.Where(t => !mapped.Any(m => LooksLike(m, t.Name)))
|
||||
.Select(t => t.Name)
|
||||
.OrderBy(n => n, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
missing.ShouldBeEmpty(
|
||||
"these drivers ship an Inspect() but the deploy-time gate does not call it, so their TagConfig "
|
||||
+ "is checked at authoring time and NOT at deploy — config arriving by CSV import or API is "
|
||||
+ "unchecked: " + string.Join(", ", missing));
|
||||
}
|
||||
|
||||
/// <summary>The map must not name a driver type with no inspector behind it — a dead key would read as
|
||||
/// coverage while dispatching nowhere.</summary>
|
||||
[Fact]
|
||||
public void Every_mapped_driver_type_resolves_to_a_real_inspector()
|
||||
{
|
||||
foreach (var driverType in EquipmentTagConfigInspector.MappedDriverTypes)
|
||||
{
|
||||
EquipmentTagConfigInspector.IsMapped(driverType).ShouldBeTrue(driverType);
|
||||
// A structurally invalid blob must come back as warnings, not an exception — the gate is on the
|
||||
// deploy path and must never throw.
|
||||
Should.NotThrow(() => EquipmentTagConfigInspector.Inspect(driverType, "{not json"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Public static <c>IReadOnlyList<string> Inspect(string)</c> is the contract every driver's
|
||||
/// deploy-time checker implements; anything else is not one of these factories.
|
||||
/// <para><b>Discovery scans the output DIRECTORY, not <c>Assembly.GetReferencedAssemblies()</c>.</b>
|
||||
/// The obvious implementation walks the reference graph — and it is circular, because the C# compiler
|
||||
/// elides an assembly reference nothing in the IL uses. Drop a driver from the dispatch map and its
|
||||
/// Contracts assembly drops out of ControlPlane's manifest too, so the sweep stops looking for
|
||||
/// exactly the driver that just went missing and the test passes. <b>That version was written, and
|
||||
/// caught only by deleting the MQTT entry and watching it stay green.</b> MSBuild copies every
|
||||
/// project-referenced assembly to the output regardless of IL usage, so the directory is the honest
|
||||
/// source.</para>
|
||||
/// <para><b>Boundary, stated so it is not mistaken for more:</b> this sees the Contracts assemblies
|
||||
/// ControlPlane project-references. A driver whose Contracts project is not referenced at all (Sql,
|
||||
/// MTConnect, Calculation, OpcUaClient) is invisible here — adding the reference is itself the act
|
||||
/// that makes this guard start demanding a map entry.</para>
|
||||
/// </summary>
|
||||
private static IReadOnlyList<Type> DiscoverInspectableFactories() =>
|
||||
Directory.GetFiles(AppContext.BaseDirectory, "ZB.MOM.WW.OtOpcUa.Driver.*.Contracts.dll")
|
||||
.Select(TryLoad)
|
||||
.Where(a => a is not null)
|
||||
.SelectMany(a => a!.GetTypes())
|
||||
.Where(t => t is { IsClass: true, IsPublic: true }
|
||||
&& t.Name.EndsWith("TagDefinitionFactory", StringComparison.Ordinal)
|
||||
&& HasInspect(t))
|
||||
.DistinctBy(t => t.FullName)
|
||||
.ToArray();
|
||||
|
||||
private static Assembly? TryLoad(string path)
|
||||
{
|
||||
try { return Assembly.LoadFrom(path); }
|
||||
catch (BadImageFormatException) { return null; }
|
||||
}
|
||||
|
||||
private static bool HasInspect(Type t)
|
||||
{
|
||||
var m = t.GetMethod("Inspect", BindingFlags.Public | BindingFlags.Static, [typeof(string)]);
|
||||
return m is not null && typeof(IReadOnlyList<string>).IsAssignableFrom(m.ReturnType);
|
||||
}
|
||||
|
||||
/// <summary>Matches a mapped driver-type string to a factory type name (<c>ModbusTagDefinitionFactory</c>
|
||||
/// ↔ <c>Modbus</c>), tolerating the casing differences between the two vocabularies
|
||||
/// (<c>FOCAS</c>/<c>Focas</c>, <c>TwinCAT</c>/<c>TwinCAT</c>).</summary>
|
||||
private static bool LooksLike(string driverType, string factoryTypeName) =>
|
||||
factoryTypeName.StartsWith(driverType, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
Reference in New Issue
Block a user