Files
333de8305b Add kitchen-sink coverage NodeSet2 with three load pipelines (#4251) (#4272)
# 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>
2026-08-17 14:59:58 -07:00

116 lines
7.2 KiB
Markdown

# 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
`DataTypeDefinition`s 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.
## 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 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.