# Description Regression fence for the NodeSet2 import + code-generation surface. This adds a single, purpose-authored "kitchen-sink" NodeSet2 (`Opc.Ua.CoverageTest.NodeSet2.xml`) plus a small dependent secondary model that deliberately flip a broad set of the knobs defined by `UANodeSet.xsd` (every node class, every `Definition` shape, custom reference types, all five modelling rules, scalar/array/matrix values, a deep branching instance tree, and a wide set of reference relations). The same bytes are then loaded into **real** servers (`ServerFixture<T>` + `ReferenceServer`) through three independent pipelines and validated node-by-node, attribute-by-attribute, reference-by-reference: 1. **Source-generation, hand-written manager** - the XML is consumed at compile time via `<AdditionalFiles ModelSourceGeneratorPrefix="Opc.Ua.CoverageTest">`; the generated `AddOpcUaCoverageTest(context)` composes the predefined nodes inside a `CustomNodeManager2`, so every node materialises as its generated `NodeState` subclass. 2. **Runtime import** - the identical XML is imported at startup through `RuntimeNodeSetNodeManagerFactory` / `AddRuntimeNodeSet`. 3. **Source-generation, generated fluent manager** - a `[NodeManager]`-attributed partial makes the generator emit the whole `FluentNodeManagerBase` manager, factory and typed fluent-builder surface. A shared, data-driven `[TestCaseSource]` battery runs the identical assertions against all three servers, and a cross-pipeline equivalence fixture asserts the three address spaces agree. A dependent **second namespace** (`.../Secondary/`) exercises cross-namespace `HasTypeDefinition`, `HasSubtype` and `HasComponent` references in every pipeline. Authoring the model surfaced two latent source-generator bugs, both fixed here: - `NodeSetToModelDesign` crashed on a top-level `<ServerUris>` table (`StringTable.Append(null)` for the reserved local-server slot). Added a null guard plus a focused regression test (`NodesetServerUrisTests`). - `FluentBuilderGenerator` emitted colliding typed-builder accessors for any model with more than one structure encoding, because `DataTypeEncoding` objects hang off their `DataType` and surfaced as parent-less "root" instances sharing the reserved `DefaultBinary`/`DefaultXml`/`DefaultJson` symbolic names. They are encoding metadata, never fluent-wired, so they are now excluded from the typed builder. A few edges are intentionally not asserted for cross-pipeline equivalence because the pipelines legitimately differ (documented in `CoverageNodeSet/README.md`): type-dictionary / `DataTypeDescription` nodes (the generator regenerates schema rather than emitting them), a handful of ambiguous duplicate-hierarchical / View-`Organizes` edges, and runtime structure stand-ins (structure definitions are validated via the `DataTypeDefinition` round-trip instead). ## Related Issues - Fixes #4251 ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
7.2 KiB
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:
- Source-generation (hand-written manager) — the XML is consumed at compile
time via
<AdditionalFiles ModelSourceGeneratorPrefix="Opc.Ua.CoverageTest">inOpc.Ua.Server.Tests.csproj. The generatedAddOpcUaCoverageTest(context)extension composes the predefined nodes inside the hand-writtenCoverageTestNodeManager : CustomNodeManager2(CoverageTestSourceGenServer). Every authored node materialises as its generatedNodeStatesubclass. - Runtime import — the identical XML is embedded as a resource and imported
at startup through
RuntimeNodeSetNodeManagerFactory/AddRuntimeNodeSet(CoverageTestRuntimeServer). - Source-generation (generated fluent manager) — a
[NodeManager]-attributed partial (CoverageTestFluentNodeManager) makes the generator emit the entireFluentNodeManagerBasemanager, 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
Definitionshape (Enumeration, OptionSet, Structure, Structure with an optional field, Union, an abstract structure, non-defaultPurpose). - Scalar, array and matrix variable value shapes.
- A deep, branching instance tree (depth 4, two branches) navigated child-by-child.
- NamespaceMetadata publication — the
NamespaceMetadataObject exists for both namespaces (all pipelines) and, for the two source-generated pipelines, surfaces the model'sNamespaceVersion(1.2.3) andNamespacePublicationDate(2024-06-01). The runtime-import pipeline carries noModelDependencyassembly attribute, so it only asserts the Object and itsNamespaceUri. - Deterministic method call — invoking the authored
AddNumbersmethod with no bound handler returnsBadNotImplementedand yields the identical status on every call. - A second (dependent) namespace with cross-namespace
HasTypeDefinition,HasSubtypeandHasComponentreferences, 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
HasModellingRuletargets, 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 /
DataTypeDescriptionnodes are not authored. The source generator regenerates the binary/XML schema from theDataTypeDefinitions rather than emittingDataTypeDictionarynodes into the address space, so hand-authored dictionary nodes would diverge. As a consequence theHasDescriptionandHasDictionaryEntryrelations are not covered here. - Duplicate hierarchical references to the same child (for example a
HasComponentalongside a custom hierarchical reference, orHasNotifieralongsideHasEventSource) and aView'sOrganizesedges 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
DataTypeDefinitionround-trip instead.
Related generator and product fixes
Authoring this model surfaced two latent source-generator issues and one product bug, all fixed:
- A top-level
<ServerUris>table crashedNodeSetToModelDesign(StringTable.Append(null)for the reserved local-server slot). The guard is intools/Opc.Ua.SourceGeneration.Core/Schema/NodeSetToModelDesign.csand is regression-tested byNodesetServerUrisTestsinOpc.Ua.SourceGeneration.Tests. - A model with more than one structure (hence more than one
Default Binaryencoding) made the generated typed fluent builder emit colliding accessors, becauseDataTypeEncodingobjects hang off theirDataTypeand surfaced as parent-less "root" instances sharing the reservedDefaultBinary/DefaultXml/DefaultJsonsymbolic names.FluentBuilderGenerator.GetTopLevelInstancesnow skipsDataTypeEncodingobjects (they are encoding metadata, never fluent-wired). NamespaceMetadataPublishernever stamped a model's publication date because the "unset" guard compared againstDateTime.MinValue(0001-01-01) while an unsetNamespacePublicationDatedefaults to the OPC UA epoch (1601-01-01); it now usesDateTimeUtc.IsNull. It also only discovered theModelDependencyattribute 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.