Files
romanett 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

225 lines
11 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System.Collections.Generic;
namespace Opc.Ua.Server.Tests.CoverageNodeSet
{
/// <summary>
/// The expected node / attribute / reference catalogue authored in
/// <c>Opc.Ua.CoverageTest.NodeSet2.xml</c>. The same catalogue drives the
/// data-driven assertion battery for both the source-generation server and
/// the runtime-import server so that any attribute or reference dropped by
/// either pipeline fails a test.
/// </summary>
public static class CoverageTestCatalogue
{
/// <summary>
/// The model namespace URI.
/// </summary>
public const string NamespaceUri = "http://opcfoundation.org/UA/CoverageTest/";
/// <summary>
/// The secondary (dependent) model namespace URI.
/// </summary>
public const string SecondaryNamespaceUri =
"http://opcfoundation.org/UA/CoverageTest/Secondary/";
/// <summary>
/// An expected node: its numeric identifier in the model namespace,
/// its BrowseName, and its NodeClass.
/// </summary>
public sealed record ExpectedNode(uint Id, string BrowseName, NodeClass NodeClass);
/// <summary>
/// An expected reference edge. <paramref name="Target"/> is either a
/// model-namespace identifier (when <paramref name="TargetIsOwned"/>)
/// or an absolute standard <see cref="NodeId"/> numeric identifier in
/// namespace 0.
/// </summary>
public sealed record ExpectedReference(
uint Source,
uint ReferenceType,
uint Target,
bool TargetIsOwned,
bool IsForward);
/// <summary>
/// Every node authored in the model namespace.
/// </summary>
public static IReadOnlyList<ExpectedNode> Nodes { get; } =
[
// Reference types.
new(5001, "CoverageHierarchicalReference", NodeClass.ReferenceType),
new(5002, "CoverageSymmetricReference", NodeClass.ReferenceType),
new(5003, "CoverageAbstractReference", NodeClass.ReferenceType),
new(5004, "CoverageConcreteReference", NodeClass.ReferenceType),
// Data types + members + encodings.
new(5100, "CoverageEnumeration", NodeClass.DataType),
new(5101, "EnumValues", NodeClass.Variable),
new(5110, "CoverageOptionSet", NodeClass.DataType),
new(5111, "OptionSetValues", NodeClass.Variable),
new(5120, "CoverageAbstractStructure", NodeClass.DataType),
new(5130, "CoverageStructure", NodeClass.DataType),
new(5131, "Default Binary", NodeClass.Object),
new(5132, "Default XML", NodeClass.Object),
new(5133, "Default JSON", NodeClass.Object),
new(5140, "CoverageOptionalStructure", NodeClass.DataType),
new(5141, "Default Binary", NodeClass.Object),
new(5150, "CoverageUnion", NodeClass.DataType),
new(5151, "Default Binary", NodeClass.Object),
new(5160, "CoveragePlainStructure", NodeClass.DataType),
new(5161, "Default Binary", NodeClass.Object),
// Object / variable / event / interface types.
new(5200, "CoverageAbstractObjectType", NodeClass.ObjectType),
new(5210, "CoverageObjectType", NodeClass.ObjectType),
new(5211, "MandatoryVariable", NodeClass.Variable),
new(5212, "OptionalVariable", NodeClass.Variable),
new(5213, "MandatoryPlaceholder", NodeClass.Object),
new(5214, "OptionalPlaceholder", NodeClass.Object),
new(5215, "ExposesArrayVariable", NodeClass.Variable),
new(5216, "MandatoryMethod", NodeClass.Method),
new(5217, "InputArguments", NodeClass.Variable),
new(5218, "OutputArguments", NodeClass.Variable),
new(5230, "CoverageEventType", NodeClass.ObjectType),
new(5240, "CoverageInterfaceType", NodeClass.ObjectType),
new(5300, "CoverageVariableType", NodeClass.VariableType),
// Instances.
new(5400, "CoverageRoot", NodeClass.Object),
new(5402, "TypedObject", NodeClass.Object),
new(5403, "MandatoryVariable", NodeClass.Variable),
new(5404, "MandatoryMethod", NodeClass.Method),
new(5405, "InputArguments", NodeClass.Variable),
new(5406, "OutputArguments", NodeClass.Variable),
new(5410, "EventSource", NodeClass.Object),
new(5411, "SubNotifier", NodeClass.Object),
new(5412, "CoverageCondition", NodeClass.Object),
new(5420, "BooleanValue", NodeClass.Variable),
new(5421, "Int32Value", NodeClass.Variable),
new(5422, "StringArrayValue", NodeClass.Variable),
new(5423, "DoubleMatrixValue", NodeClass.Variable),
new(5424, "DateTimeValue", NodeClass.Variable),
new(5425, "EnumerationValue", NodeClass.Variable),
new(5426, "OrderedValue", NodeClass.Variable),
new(5450, "AddNumbers", NodeClass.Method),
new(5451, "LockedMethod", NodeClass.Method),
new(5452, "InputArguments", NodeClass.Variable),
new(5453, "OutputArguments", NodeClass.Variable),
new(5460, "CoverageView", NodeClass.View),
new(5470, "CoverageStateMachine", NodeClass.Object),
new(5471, "StateA", NodeClass.Object),
new(5472, "StateB", NodeClass.Object),
new(5473, "TransitionAB", NodeClass.Object),
// Deep, branching instance tree.
new(5490, "TreeRoot", NodeClass.Object),
new(5491, "BranchA", NodeClass.Object),
new(5492, "LeafA1", NodeClass.Variable),
new(5493, "SubBranchA", NodeClass.Object),
new(5494, "LeafA2", NodeClass.Variable),
new(5495, "BranchB", NodeClass.Object),
new(5496, "LeafB1", NodeClass.Variable),
];
/// <summary>
/// Every node authored in the secondary (dependent) model namespace.
/// </summary>
public static IReadOnlyList<ExpectedNode> SecondaryNodes { get; } =
[
new(6001, "SecondaryObjectType", NodeClass.ObjectType),
new(6010, "SecondaryInstance", NodeClass.Object),
];
/// <summary>
/// At least one forward (or explicitly authored inverse) occurrence of
/// every reference relation the model exercises.
/// </summary>
public static IReadOnlyList<ExpectedReference> References { get; } =
[
// Hierarchical.
new(5400, ReferenceTypes.Organizes, Objects.ObjectsFolder, false, false),
new(5400, ReferenceTypes.HasComponent, 5424, true, true),
new(5400, ReferenceTypes.HasOrderedComponent, 5426, true, true),
new(5100, ReferenceTypes.HasProperty, 5101, true, true),
new(5200, ReferenceTypes.HasSubtype, 5210, true, true),
new(5410, ReferenceTypes.HasEventSource, 5411, true, true),
// Non-hierarchical / instance wiring.
new(5420, ReferenceTypes.HasTypeDefinition, VariableTypes.BaseDataVariableType, false, true),
new(5211, ReferenceTypes.HasModellingRule, ObjectIds_ModellingRule_Mandatory, false, true),
new(5212, ReferenceTypes.HasModellingRule, ObjectIds_ModellingRule_Optional, false, true),
new(5213, ReferenceTypes.HasModellingRule, ObjectIds_ModellingRule_MandatoryPlaceholder, false, true),
new(5214, ReferenceTypes.HasModellingRule, ObjectIds_ModellingRule_OptionalPlaceholder, false, true),
new(5215, ReferenceTypes.HasModellingRule, ObjectIds_ModellingRule_ExposesItsArray, false, true),
new(5130, ReferenceTypes.HasEncoding, 5131, true, true),
new(5210, ReferenceTypes.HasInterface, 5240, true, true),
new(5210, ReferenceTypes.GeneratesEvent, 5230, true, true),
new(5216, ReferenceTypes.AlwaysGeneratesEvent, 5230, true, true),
new(5410, ReferenceTypes.HasCondition, 5412, true, true),
// State machine.
new(5473, ReferenceTypes.FromState, 5471, true, true),
new(5473, ReferenceTypes.ToState, 5472, true, true),
new(5473, ReferenceTypes.HasCause, 5450, true, true),
new(5473, ReferenceTypes.HasEffect, 5230, true, true),
new(5471, ReferenceTypes.HasTrueSubState, 5472, true, true),
new(5471, ReferenceTypes.HasFalseSubState, 5472, true, true),
// Custom reference types + inverse authoring.
new(5400, 5001, 5420, true, true),
new(5400, 5002, 5421, true, true),
new(5400, 5004, 5422, true, true),
// Deep instance tree (nested HasComponent chain).
new(5400, ReferenceTypes.HasComponent, 5490, true, true),
new(5490, ReferenceTypes.HasComponent, 5491, true, true),
new(5491, ReferenceTypes.HasComponent, 5493, true, true),
new(5493, ReferenceTypes.HasComponent, 5494, true, true),
];
// Standard modelling-rule object identifiers (namespace 0).
private const uint ObjectIds_ModellingRule_Mandatory = 78;
private const uint ObjectIds_ModellingRule_Optional = 80;
private const uint ObjectIds_ModellingRule_ExposesItsArray = 83;
private const uint ObjectIds_ModellingRule_MandatoryPlaceholder = 11508;
private const uint ObjectIds_ModellingRule_OptionalPlaceholder = 11510;
/// <summary>
/// Builds the model-namespace <see cref="NodeId"/> for an owned node.
/// </summary>
public static NodeId NodeId(uint id, ushort namespaceIndex)
{
return new NodeId(id, namespaceIndex);
}
}
}