refactor(opcuaclient): move NamespaceMap to a .Session leaf so Contracts is dependency-light
G-7 wired OpcUaClientTagDefinitionFactory.Inspect into the deploy-time TagConfig gate, which gave ControlPlane a ProjectReference to Driver.OpcUaClient.Contracts — and with it, transitively, the whole OPC UA client stack (5 OPCFoundation packages) into every host that loads the control plane. NamespaceMap was the only reason Contracts carried OPCFoundation.NetStandard.Opc.Ua.Client: its entire public surface is ISession/NamespaceTable/NodeId-typed. It moves to a new leaf project, ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session, referenced by the runtime .Driver and by .Browser. Contracts is left with Core.Abstractions and NOTHING else — the same shape as Calculation.Contracts. The namespace stays ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient, so no consumer's `using` changed. The .Session -> .Contracts direction is deliberate: the blob read NamespaceMap needs is OpcUaClientTagDefinitionFactory.TryReadNodeId, the one authority it shares with the deploy gate, and that must stay in the dependency-light half. Why a new project rather than folding NamespaceMap into the runtime .Driver and having .Browser reference it (the Driver.Mqtt.Browser #512-style deliberate exception): the AdminUI project-references .Browser and NOT .Driver, so that shape would pull the OpcUaClient driver runtime — plus Core, Opc.Ua.Configuration and Secrets.Abstractions — into the AdminUI's build/deploy graph. That is exactly the violation #512 spent two extractions removing from Mqtt.Browser; re-introducing it to save one csproj is a bad trade. Guard: ContractsIsDependencyLightTests (Driver.OpcUaClient.Tests) asserts the Contracts manifest names no Opc.Ua* assembly, plus a counterpart asserting .Session does. An ABSENCE assertion is sound on the manifest where a presence one is not — the compiler elides an unused reference, but it MUST emit one the moment Opc.Ua-typed code is added. Falsified by temporarily re-adding the package plus an Opc.Ua.NodeId-typed member: red on Opc.Ua.Types, green again on removal. Evidence — `dotnet list ControlPlane package --include-transitive | grep -i opc`: before: OPCFoundation.NetStandard.Opc.Ua.{Client,Configuration,Core, Security.Certificates,Types} 1.5.378.106 after: none (and 0 Opc.Ua*.dll in ControlPlane's output directory)
This commit is contained in:
@@ -48,6 +48,7 @@
|
|||||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Contracts/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Contracts.csproj" />
|
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Contracts/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Contracts.csproj" />
|
||||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj" />
|
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj" />
|
||||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts.csproj" />
|
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts.csproj" />
|
||||||
|
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session.csproj" />
|
||||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.csproj" />
|
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.csproj" />
|
||||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.csproj" />
|
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.csproj" />
|
||||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Contracts/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Contracts.csproj" />
|
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Contracts/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Contracts.csproj" />
|
||||||
|
|||||||
+4
@@ -8,6 +8,10 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Commons\ZB.MOM.WW.OtOpcUa.Commons.csproj" />
|
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Commons\ZB.MOM.WW.OtOpcUa.Commons.csproj" />
|
||||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts.csproj" />
|
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts.csproj" />
|
||||||
|
<!-- NamespaceMap. Deliberately the .Session leaf and NOT the runtime .Driver project: the AdminUI
|
||||||
|
references THIS project, so a .Driver reference would pull the driver runtime into the
|
||||||
|
AdminUI's deploy graph (the Gitea #512 shape). See the .Session csproj block comment. -->
|
||||||
|
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" />
|
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" />
|
||||||
|
|||||||
+2
-1
@@ -31,7 +31,8 @@ public sealed record OpcUaClientDriverOptions
|
|||||||
/// with the cluster's raw OPC UA Client tags. Each <see cref="RawTagEntry"/> carries the tag's
|
/// with the cluster's raw OPC UA Client tags. Each <see cref="RawTagEntry"/> carries the tag's
|
||||||
/// <b>RawPath</b> identity plus a driver <c>TagConfig</c> blob whose <c>nodeId</c> key holds the
|
/// <b>RawPath</b> identity plus a driver <c>TagConfig</c> blob whose <c>nodeId</c> key holds the
|
||||||
/// upstream OPC UA node id string the driver dials. The driver builds a <c>RawPath → nodeId</c>
|
/// upstream OPC UA node id string the driver dials. The driver builds a <c>RawPath → nodeId</c>
|
||||||
/// resolution table (via <see cref="NamespaceMap"/>) from this list at connect time; a
|
/// resolution table (via <c>NamespaceMap</c>, in the sibling <c>.Session</c> project) from this
|
||||||
|
/// list at connect time; a
|
||||||
/// read/write/subscribe/history reference is always a RawPath resolved through that table.
|
/// read/write/subscribe/history reference is always a RawPath resolved through that table.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<RawTagEntry> RawTags { get; init; } = [];
|
public IReadOnlyList<RawTagEntry> RawTags { get; init; } = [];
|
||||||
|
|||||||
+9
-3
@@ -4,13 +4,19 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The authored-raw-tag reader for the OPC UA Client (gateway) driver: one authority, consumed by both
|
/// The authored-raw-tag reader for the OPC UA Client (gateway) driver: one authority, consumed by both
|
||||||
/// the runtime (<see cref="NamespaceMap"/>'s <c>RawPath → upstream node id</c> table build) and the
|
/// the runtime (<c>NamespaceMap</c>'s <c>RawPath → upstream node id</c> table build) and the
|
||||||
/// deploy-time TagConfig gate (<see cref="Inspect"/>, deferment-register G-7).
|
/// deploy-time TagConfig gate (<see cref="Inspect"/>, deferment-register G-7).
|
||||||
/// <para>
|
/// <para>
|
||||||
/// <b>This read used to be private to <see cref="NamespaceMap"/>.</b> It moved here — unchanged —
|
/// <b>This read used to be private to <c>NamespaceMap</c>.</b> It moved here — unchanged —
|
||||||
/// rather than being mirrored: a mirror of an accept/reject boundary drifts the moment either side
|
/// rather than being mirrored: a mirror of an accept/reject boundary drifts the moment either side
|
||||||
/// is touched, and the drift is silent, with the gate describing a shape the driver no longer
|
/// is touched, and the drift is silent, with the gate describing a shape the driver no longer
|
||||||
/// reads. <see cref="NamespaceMap"/> now calls <see cref="TryReadNodeId"/>.
|
/// reads. <c>NamespaceMap</c> now calls <see cref="TryReadNodeId"/>.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <b><c>NamespaceMap</c> is named by text, not <c>cref</c>, on purpose.</b> It lives in the
|
||||||
|
/// sibling <c>Driver.OpcUaClient.Session</c> project, which references THIS one — the dependency
|
||||||
|
/// runs that way round so this assembly stays dependency-light for ControlPlane, which
|
||||||
|
/// project-references it for the G-7 gate. A <c>cref</c> here would need the reverse reference.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// <b><see cref="Inspect"/> is never stricter than <see cref="TryReadNodeId"/></b> — it asks the
|
/// <b><see cref="Inspect"/> is never stricter than <see cref="TryReadNodeId"/></b> — it asks the
|
||||||
|
|||||||
+6
-5
@@ -6,11 +6,12 @@
|
|||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" />
|
<!-- DEPENDENCY-LIGHT ON PURPOSE: Core.Abstractions and nothing else, the same shape as
|
||||||
</ItemGroup>
|
Calculation.Contracts. ControlPlane project-references this for the G-7 deploy-time
|
||||||
<ItemGroup>
|
TagConfig gate (OpcUaClientTagDefinitionFactory.Inspect), so anything added here lands in
|
||||||
<!-- v3: NamespaceMap resolves a RawPath (RawTagEntry identity) to the upstream node id.
|
every host that loads the control plane. The OPC UA client SDK used to be here, for
|
||||||
RawTagEntry lives in the Core.Abstractions leaf beside EquipmentTagRefResolver. -->
|
NamespaceMap; NamespaceMap now lives in the sibling .Session leaf. Pinned by
|
||||||
|
ContractsIsDependencyLightTests (Driver.OpcUaClient.Tests). -->
|
||||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+11
@@ -52,6 +52,17 @@ public readonly record struct RawTagBinding(string NodeId, bool WriteIdempotent)
|
|||||||
/// driver then re-binds against the live session via the static
|
/// driver then re-binds against the live session via the static
|
||||||
/// <see cref="TryResolve(ISession?, string, out NodeId)"/>.
|
/// <see cref="TryResolve(ISession?, string, out NodeId)"/>.
|
||||||
/// </para>
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <b>Why this type sits in <c>.Session</c> and not <c>.Contracts</c>.</b> Its whole public
|
||||||
|
/// surface is OPC UA SDK-typed (<see cref="ISession"/>, <see cref="NamespaceTable"/>,
|
||||||
|
/// <see cref="NodeId"/>), so while it lived in <c>.Contracts</c> that project had to carry
|
||||||
|
/// <c>OPCFoundation.NetStandard.Opc.Ua.Client</c> — and once ControlPlane project-referenced
|
||||||
|
/// <c>.Contracts</c> for the G-7 deploy-time TagConfig gate, the whole OPC UA client stack
|
||||||
|
/// came with it into every host that loads the control plane. The blob read this class needs
|
||||||
|
/// stayed behind in <c>.Contracts</c> as
|
||||||
|
/// <c>OpcUaClientTagDefinitionFactory.TryReadNodeId</c> (the ONE authority the deploy gate and
|
||||||
|
/// this table share), which is why the reference runs <c>.Session → .Contracts</c>.
|
||||||
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class NamespaceMap
|
public sealed class NamespaceMap
|
||||||
{
|
{
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<!-- Namespace stays ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient (no .Session suffix) so moving
|
||||||
|
NamespaceMap here changed no consumer's `using`. -->
|
||||||
|
<RootNamespace>ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient</RootNamespace>
|
||||||
|
<AssemblyName>ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session</AssemblyName>
|
||||||
|
</PropertyGroup>
|
||||||
|
<!--
|
||||||
|
The live-OPC-UA-session layer of the OpcUaClient driver: everything the driver AND the AdminUI's
|
||||||
|
browser both need that is typed in ISession / NodeId / NamespaceTable. Today that is exactly
|
||||||
|
NamespaceMap.
|
||||||
|
|
||||||
|
WHY IT IS NOT IN .Contracts. It used to be, and .Contracts therefore carried
|
||||||
|
OPCFoundation.NetStandard.Opc.Ua.Client. When G-7 wired OpcUaClientTagDefinitionFactory.Inspect into
|
||||||
|
the deploy-time TagConfig gate, ControlPlane gained a ProjectReference to .Contracts — and with it
|
||||||
|
the whole OPC UA client stack, transitively, into every host that loads the control plane. A
|
||||||
|
Contracts project is meant to be the dependency-light parse/shape layer (see Calculation.Contracts:
|
||||||
|
Core.Abstractions and nothing else), so the SDK-typed half moved out rather than the gate doing
|
||||||
|
without.
|
||||||
|
|
||||||
|
WHY NOT JUST PUT IT IN THE .Driver PROJECT (the Mqtt.Browser-style deliberate exception). The
|
||||||
|
AdminUI project-references .Browser and NOT the runtime .Driver. Having .Browser reference .Driver
|
||||||
|
to reach NamespaceMap would pull the whole driver runtime — plus Core, Opc.Ua.Configuration and
|
||||||
|
Secrets.Abstractions — into the AdminUI's build/deploy graph. That is precisely the violation Gitea
|
||||||
|
#512 spent two extractions removing from Driver.Mqtt.Browser; re-introducing the same shape here to
|
||||||
|
save one csproj would be trading a measured regression for a little less churn.
|
||||||
|
|
||||||
|
KEEP THIS LEAF THIN. Anything addable here that is NOT session-typed belongs in .Contracts, or the
|
||||||
|
dependency-light property this split exists to buy erodes one file at a time. The pin is
|
||||||
|
ContractsIsDependencyLightTests in Driver.OpcUaClient.Tests.
|
||||||
|
-->
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- RawTagEntry (the v3 authored-tag identity NamespaceMap indexes) lives in the Core.Abstractions
|
||||||
|
leaf; OpcUaClientTagDefinitionFactory is the ONE authority for reading a TagConfig blob's
|
||||||
|
nodeId, shared with the deploy gate — NamespaceMap calls it rather than re-parsing. -->
|
||||||
|
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
||||||
|
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts.csproj"/>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
+3
@@ -14,6 +14,9 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts.csproj"/>
|
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts.csproj"/>
|
||||||
|
<!-- NamespaceMap (ISession-typed) — shared with .Browser, which is why it is its own leaf rather
|
||||||
|
than living here. See the block comment in the .Session csproj. -->
|
||||||
|
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session.csproj"/>
|
||||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
||||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core\ZB.MOM.WW.OtOpcUa.Core.csproj"/>
|
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core\ZB.MOM.WW.OtOpcUa.Core.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -54,10 +54,8 @@
|
|||||||
TagConfigValidator checked them. Adding the reference is what makes
|
TagConfigValidator checked them. Adding the reference is what makes
|
||||||
EquipmentTagConfigInspectorCoverageTests demand a dispatch-map entry for them — the guard scans
|
EquipmentTagConfigInspectorCoverageTests demand a dispatch-map entry for them — the guard scans
|
||||||
the output directory, so an unreferenced Contracts assembly is invisible to it.
|
the output directory, so an unreferenced Contracts assembly is invisible to it.
|
||||||
Note OpcUaClient.Contracts brings OPCFoundation.NetStandard.Opc.Ua.Client transitively: its
|
OpcUaClient.Contracts is dependency-light (Core.Abstractions only) — the OPC UA client SDK it
|
||||||
NamespaceMap re-binds a stable nsu= reference against a live session. Every host that loads
|
used to pull in transitively left with NamespaceMap, to Driver.OpcUaClient.Session. -->
|
||||||
ControlPlane (Host, AdminUI) already carries the OPC UA stack, so this adds no new deployment
|
|
||||||
surface. -->
|
|
||||||
<ProjectReference Include="..\..\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts\ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts.csproj"/>
|
<ProjectReference Include="..\..\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts\ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts.csproj"/>
|
||||||
<ProjectReference Include="..\..\Drivers\ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts\ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts.csproj"/>
|
<ProjectReference Include="..\..\Drivers\ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts\ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts.csproj"/>
|
||||||
<ProjectReference Include="..\..\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Calculation.Contracts\ZB.MOM.WW.OtOpcUa.Driver.Calculation.Contracts.csproj"/>
|
<ProjectReference Include="..\..\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Calculation.Contracts\ZB.MOM.WW.OtOpcUa.Driver.Calculation.Contracts.csproj"/>
|
||||||
|
|||||||
+3
-1
@@ -22,7 +22,9 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.csproj"/>
|
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.csproj"/>
|
||||||
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts.csproj"/>
|
<!-- NamespaceMap.FromTable — it moved from .Contracts to the .Session leaf when .Contracts was
|
||||||
|
made dependency-light (ControlPlane references .Contracts for the G-7 deploy gate). -->
|
||||||
|
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session.csproj"/>
|
||||||
<ProjectReference Include="..\..\..\src\Core\ZB.MOM.WW.OtOpcUa.Commons\ZB.MOM.WW.OtOpcUa.Commons.csproj"/>
|
<ProjectReference Include="..\..\..\src\Core\ZB.MOM.WW.OtOpcUa.Commons\ZB.MOM.WW.OtOpcUa.Commons.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pins the dependency shape of <c>ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts</c>: it must not
|
||||||
|
/// reference the OPC UA client SDK.
|
||||||
|
/// <para>
|
||||||
|
/// <b>Why this matters beyond tidiness.</b> Deferment-register G-7 wired
|
||||||
|
/// <see cref="OpcUaClientTagDefinitionFactory.Inspect"/> into the deploy-time TagConfig gate, which
|
||||||
|
/// made <c>ControlPlane</c> project-reference this Contracts assembly. ControlPlane is loaded by
|
||||||
|
/// every admin- and driver-role host, so whatever Contracts drags in, they all carry. Contracts
|
||||||
|
/// used to hold <c>NamespaceMap</c> — <c>ISession</c>/<c>NodeId</c>-typed throughout — and with it a
|
||||||
|
/// <c>OPCFoundation.NetStandard.Opc.Ua.Client</c> PackageReference; NamespaceMap moved to the
|
||||||
|
/// sibling <c>.Session</c> leaf so the parse-only half could stay light (the same shape as
|
||||||
|
/// <c>Calculation.Contracts</c>: Core.Abstractions and nothing else).
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <b>Direction matters: this is an ABSENCE assertion, and that is what makes it sound here.</b>
|
||||||
|
/// The assembly manifest is an unreliable witness for PRESENCE — the C# compiler elides a reference
|
||||||
|
/// nothing in the IL actually uses, so "assembly X is referenced" can be false while the csproj says
|
||||||
|
/// otherwise (the trap <c>EquipmentTagConfigInspectorCoverageTests</c> documents, which is why that
|
||||||
|
/// one scans the output directory instead). Absence has no such hole in the direction that matters:
|
||||||
|
/// the moment someone adds Opc.Ua-typed code to Contracts, the compiler MUST emit the reference and
|
||||||
|
/// this test goes red. A csproj PackageReference that nothing consumes is elided and slips past —
|
||||||
|
/// accepted deliberately, because an unused package reference is not the failure being guarded.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Falsified 2026-08-09 by temporarily adding an <c>Opc.Ua.NodeId</c>-typed member to Contracts:
|
||||||
|
/// the assertion failed naming <c>Opc.Ua.Core</c>, and passed again once removed.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ContractsIsDependencyLightTests
|
||||||
|
{
|
||||||
|
/// <summary>The Contracts assembly's manifest must name no OPC UA SDK assembly.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void The_Contracts_assembly_does_not_reference_the_OPC_UA_SDK()
|
||||||
|
{
|
||||||
|
var contracts = typeof(OpcUaClientTagDefinitionFactory).Assembly;
|
||||||
|
|
||||||
|
// Positive control: name the assembly under test, so a rename/retarget cannot silently point this
|
||||||
|
// at something else that trivially satisfies the assertion below.
|
||||||
|
contracts.GetName().Name.ShouldBe("ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts");
|
||||||
|
|
||||||
|
var opcUa = contracts.GetReferencedAssemblies()
|
||||||
|
.Select(a => a.Name ?? string.Empty)
|
||||||
|
.Where(n => n.StartsWith("Opc.Ua", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.OrderBy(n => n, StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
opcUa.ShouldBeEmpty(
|
||||||
|
"OpcUaClient.Contracts must stay free of the OPC UA client SDK — ControlPlane project-references "
|
||||||
|
+ "it for the G-7 deploy-time TagConfig gate, so every host that loads the control plane would "
|
||||||
|
+ "carry the stack. Put SDK-typed code in Driver.OpcUaClient.Session instead. Found: "
|
||||||
|
+ string.Join(", ", opcUa));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The counterpart the assertion above is meaningful against: the SDK-typed half really did land
|
||||||
|
/// somewhere, and that somewhere does reference the SDK. Without this, "no Opc.Ua reference
|
||||||
|
/// anywhere" would read as a pass.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void The_Session_assembly_is_where_the_OPC_UA_SDK_reference_lives()
|
||||||
|
{
|
||||||
|
var session = typeof(NamespaceMap).Assembly;
|
||||||
|
|
||||||
|
session.GetName().Name.ShouldBe("ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session");
|
||||||
|
session.GetReferencedAssemblies()
|
||||||
|
.Select(a => a.Name ?? string.Empty)
|
||||||
|
.ShouldContain(n => n.StartsWith("Opc.Ua", StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user