Files
Marc SchierandCopilot 81f200c9d2 Align WoT conversion and runtime with Binding 1.1 (#4375)
## Summary

Align the generic WoT/NodeSet converter, WoT Connectivity registry, and
runtime
binding implementation with the OPC UA WoT Binding 1.1 draft.

### Conversion and preservation

- fix the standard placeholder ModellingRule NodeIds;
- make every produced/restored NodeSet importable by completing required
aliases;
- support `ua:ComponentOf`, arbitrary companion ReferenceTypes,
BrowseNames,
  InverseNames, symmetric references, and exact direction;
- enforce the projection annotation allowlist and deterministic total
order;
- emit and consume Method argument schemas, EventType data, Alarms &
Conditions,
  event severity, engineering units/ranges, localization, ValueRank, and
  ArrayDimensions;
- preserve unsupported content through JSON residue/native projection
rather than
  silently dropping it;
- compare residue conflicts with RFC 8785 canonical equality while
retaining
  byte-exact residue digests;
- stamp generated documents with `uav:bindingVersion: "1.1"`.

### Runtime bindings

- resolve an EventType Thing Model referenced with `tm:ref`, recursively
derive
  its `data` fields as the baseline selection, and overlay explicit
  `uav:eventSelectClauses` that also reference EventType TMs;
- pre-resolve EventType links asynchronously through the bounded
sibling-document
  resolver before synchronous protocol planning;
- retain read compatibility with legacy implementation-specific
`uav:eventFields`;
- expose nested event `data` matching the TD while retaining the flat
transport
  field index for compatibility;
- apply minimum-security constraints through deterministic endpoint
selection and
  fail-closed session verification;
- use one Unicode code-point comparer for projection membership and
endpoint ties.

### Validation and fixtures

- add strict, profile-aware validation while keeping default parsing
permissive and
  residue-preserving;
- pin 27 normative examples byte-for-byte to the specification
repository, with an
  offline SHA-256 manifest and regeneration test;
- regenerate checked-in WoT documents with the final canonical writer
and binding
  version;
- document the generic converter separately from the legacy OPC 10100-1
asset path.

## Notable compatibility changes

- converted NodeSets may gain a completed `<Aliases>` table so standard
  ReferenceType names can be imported;
- previously inverted/dangling placeholder ModellingRules are corrected;
- generated documents gain binding-version, localization,
rank/dimension, and exact
  ReferenceType metadata;
- event data now has the nested shape described by the binding;
- the former NodeId-based `uav:typeDefinitionId` clause is removed;
event
  selections now use EventType-TM `tm:ref` links;
- `uav:eventFields` remains readable but new documents use
  `uav:eventSelectClauses`;
- unknown well-formed future revision/profile claims produce unsupported
warnings
  rather than strict rejection.

## Generated files

Most of the 621-file diff is deterministic regeneration: 517 checked-in
WoT
documents gain the binding version and final canonical spelling.
Regeneration was
run twice and is byte-idempotent.

## Validation

- `dotnet restore UA.slnx`;
- `dotnet build UA.slnx -c Release` across all configured TFMs;
- `Opc.Ua.Types.Tests`: 9,215 net10.0; 9,208 net48; 9,208 net472;
- `Opc.Ua.WotCon.Bindings.Tests`: 611 each on net10.0, net9.0, and
net8.0;
- `Opc.Ua.WotCon.Tests`: 1,136 net10.0;
- `Opc.Ua.WotCon.Samples.Tests`: 14 net10.0;
- NativeAOT test executable: 136/136;
- all 27 strict fixture conversion/import checks;
- formatting, analyzers, `git diff --check`, and regeneration
idempotence.

The full net48 run retains one unrelated existing MAX_PATH failure in

`FileWotRegistryStoreTests.MultiLevelDirectoryCreationSyncsEveryModifiedParentInOrder`.
Solution-wide stress runs also include an intentional 60-minute soak and
unrelated
locale-sensitive/flaky tests; all affected WoT projects and tests pass.

## Specification

Depends on OPCF-Members/spec-drafts#31.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6a9f41b1-72f0-4c74-bd7f-26fa531e5738
2026-09-02 00:09:36 +00:00
..

Coverage NodeSet test harness (issue #4251)

This directory contains a purpose-authored "kitchen-sink" NodeSet2 (Assets/Opc.Ua.CoverageTest.NodeSet2.xml) plus a small dependent secondary model (Assets/Opc.Ua.CoverageTestSecondary.NodeSet2.xml) that deliberately flips a broad set of the knobs defined by UANodeSet.xsd, and a data-driven assertion battery that runs the same checks against the model loaded through three independent pipelines:

  1. Source-generation (hand-written manager) — the XML is consumed at compile time via <AdditionalFiles ModelSourceGeneratorPrefix="Opc.Ua.CoverageTest"> in Opc.Ua.Server.Tests.csproj. The generated AddOpcUaCoverageTest(context) extension composes the predefined nodes inside the hand-written CoverageTestNodeManager : CustomNodeManager2 (CoverageTestSourceGenServer). Every authored node materialises as its generated NodeState subclass.
  2. Runtime import — the identical XML is embedded as a resource and imported at startup through RuntimeNodeSetNodeManagerFactory / AddRuntimeNodeSet (CoverageTestRuntimeServer).
  3. Source-generation (generated fluent manager) — a [NodeManager]-attributed partial (CoverageTestFluentNodeManager) makes the generator emit the entire FluentNodeManagerBase manager, factory and typed fluent-builder surface (CoverageTestFluentGenServer).

Each server is started for real (ServerFixture<T> + ReferenceServer), and the assertion battery is executed once per server, then a cross-pipeline equivalence fixture asserts the three address spaces agree.

Files

File Purpose
Assets/Opc.Ua.CoverageTest.NodeSet2.xml The primary kitchen-sink model.
Assets/Opc.Ua.CoverageTest.NodeSet2.csv Matching SymbolicName/Id/NodeClass table for byte-stable ids.
Assets/Opc.Ua.CoverageTestSecondary.NodeSet2.xml A dependent second-namespace model (cross-namespace references).
CoverageTestCatalogue.cs The expected node + reference catalogue (drives the data-driven tests).
CoverageTestSourceGenServer.cs Source-generation host (CoverageTestNodeManager), owns both namespaces.
CoverageTestRuntimeServer.cs Runtime-import host (two dependency-sorted sources).
CoverageTestFluentNodeManager.cs / CoverageTestSecondaryFluentNodeManager.cs [NodeManager] partials for the generated fluent managers.
CoverageTestFluentGenServer.cs Generated-fluent-manager host.
CoverageNodeSetAssertionsBase.cs The shared, data-driven assertion battery (generic over the server type).
CoverageTestAssertionsTests.cs Concrete fixtures binding the battery to each pipeline.
CoverageTestEquivalenceTests.cs Cross-pipeline structural equivalence fixture (all three servers).

What is exercised

  • Every node class (UAReferenceType, UADataType, UAObjectType, UAVariableType, UAObject, UAVariable, UAMethod, UAView).
  • Every Definition shape (Enumeration, OptionSet, Structure, Structure with an optional field, Union, an abstract structure, non-default Purpose).
  • Scalar, array and matrix variable value shapes.
  • A deep, branching instance tree (depth 4, two branches) navigated child-by-child.
  • NamespaceMetadata publication — the NamespaceMetadata Object exists for both namespaces (all pipelines) and, for the two source-generated pipelines, surfaces the model's NamespaceVersion (1.2.3) and NamespacePublicationDate (2024-06-01). The runtime-import pipeline carries no ModelDependency assembly attribute, so it only asserts the Object and its NamespaceUri.
  • Deterministic method call — invoking the authored AddNumbers method with no bound handler returns BadNotImplemented and yields the identical status on every call.
  • A second (dependent) namespace with cross-namespace HasTypeDefinition, HasSubtype and HasComponent references, hosted in all three pipelines.
  • Custom reference types: hierarchical/asymmetric (with InverseName), symmetric, an abstract type with a concrete subtype, plus inverse authoring.
  • Non-default common/instance/variable/method/view attributes, all five HasModellingRule targets, and a broad set of reference relations (HasComponent, HasProperty, HasOrderedComponent, HasSubtype, HasTypeDefinition, HasEncoding, HasEventSource, HasCondition, HasInterface, GeneratesEvent/AlwaysGeneratesEvent, FromState/ToState/ HasCause/HasEffect, HasTrueSubState/HasFalseSubState, and the custom reference types).

Known pipeline differences (intentionally not asserted for equivalence)

The reference catalogue deliberately excludes edges where the two pipelines legitimately differ, so the equivalence fixture stays strict on everything else:

  • Type dictionaries / DataTypeDescription nodes are not authored. The source generator regenerates the binary/XML schema from the DataTypeDefinitions rather than emitting DataTypeDictionary nodes into the address space, so hand-authored dictionary nodes would diverge. As a consequence the HasDescription and HasDictionaryEntry relations are not covered here.
  • Duplicate hierarchical references to the same child (for example a HasComponent alongside a custom hierarchical reference, or HasNotifier alongside HasEventSource) and a View's Organizes edges to already-parented nodes are represented differently by the two importers, so those specific edges are not asserted for equivalence.
  • Runtime structure stand-ins — the runtime complex-type loader registers the enumeration and option-set stand-ins for this multi-type model but not the structure stand-ins; the structure definitions are validated directly via the DataTypeDefinition round-trip instead.

Authoring this model surfaced two latent source-generator issues and one product bug, all fixed:

  • A top-level <ServerUris> table crashed NodeSetToModelDesign (StringTable.Append(null) for the reserved local-server slot). The guard is in tools/Opc.Ua.SourceGeneration.Core/Schema/NodeSetToModelDesign.cs and is regression-tested by NodesetServerUrisTests in Opc.Ua.SourceGeneration.Tests.
  • A model with more than one structure (hence more than one Default Binary encoding) made the generated typed fluent builder emit colliding accessors, because DataTypeEncoding objects hang off their DataType and surfaced as parent-less "root" instances sharing the reserved DefaultBinary/DefaultXml/ DefaultJson symbolic names. FluentBuilderGenerator.GetTopLevelInstances now skips DataTypeEncoding objects (they are encoding metadata, never fluent-wired).
  • NamespaceMetadataPublisher never stamped a model's publication date because the "unset" guard compared against DateTime.MinValue (0001-01-01) while an unset NamespacePublicationDate defaults to the OPC UA epoch (1601-01-01); it now uses DateTimeUtc.IsNull. It also only discovered the ModelDependency attribute through a node manager's sync facade, which is a framework wrapper for async-native (fluent) managers; it now scans both the async manager's and its sync facade's assemblies so the fluent pipeline publishes version metadata too.