Files
Marc Schier da7412c429 Add native xRegistry events and distinct Resource versions (#4396)
## Summary

- support the xRegistry 0.6.0 distinct Resource/Version hierarchy:
  `Registry -> Group -> Resource -> Versions -> Version`
- instantiate stable logical Resource nodes separately from exact
Version nodes
  while preserving existing Version NodeIds
- delegate logical Resource FileType operations and Version-view
metadata to the
selected default Version with handle pinning and shared writer
reservations
- make Resource and Version delete behavior role-stable and use
`MetaEpoch` /
  `Epoch` respectively
- use stable Resource SourceNodes, exact Version SourceNodes and the
  `Server -> Registry -> Group -> Resource -> Version` notifier chain
- keep a model-metadata-gated client fallback for 0.5 flat Servers; 0.6
Servers
  expose no legacy aliases
- target `HasWoTProjection` at the real stable logical Resource node
- use `AsyncCustomNodeManager` for the registration, fast-path, and
federation managers,
  with awaited lifecycle, node mutations, event delivery, and cleanup
- handle startup rollback, reentrant deletion callbacks, undisclosed
file handles,
  and retryable cleanup without blocking unrelated file operations

## Authoritative dependencies

- companion specification: OPCF-Members/spec-drafts#32 at
  `d3399fca025025a73e5a6b599f850a81d985c060`
- OPC UA binding: xregistry/spec#568
- xRegistry event semantics: xregistry/spec#563, merged as
  `66ee31e47fbadff6ab982ae1516f7d9065e46b64`
- companion NodeSet SHA-256:
  `369f5f3f1814b333e4eeb52bfb4b6a730cdff372505a5f91a54c94f0016ac4cf`

## Review feedback

- keep the WoT Connectivity information-model designation at `1.1`;
draft revisions update the publication date without changing the model
version
- eliminate all 198 unique CA1861 diagnostics across 54 files and remove
seven obsolete project suppressions
- fix the analyzer-clean touched-path CA1307 and the hierarchy follow-up
CA1846/CA1851 diagnostics without dropping legacy-TFM compatibility

- recover the reconcile queue after callback faults/cancellation and
continue accepted pending changes
- remove migration guidance for the xRegistry managers, which are new in
2.0

## Compatibility

- public WoT Connectivity model designation remains `1.1`
- generated event-record ABI, constructors, options, NodeIds, and stored
documents remain compatible
- the generic xRegistry managers are new in 2.0 and implement
`IAsyncNodeManager`;
use `IAsyncNodeManagerFactory` and the async lifecycle hooks (see
`docs\XRegistry.md`)
- persistence schema and document blobs remain compatible unless noted
below

## Validation

- merge validation at `980422f76` with upstream `e8bec27d5`: 454
targeted WoT/materialization/model/registry cases and all 327 XRegistry
cases passed on both net10.0 and net48; all 29 net10 aggregation sample
cases passed
- feedback regressions at `308e8ba74`: 55 targeted queue and live
registry event/projection/lifecycle cases passed on each of net10.0 and
net48
- prior CI: all 266 checks passed at `6489a0908`; the unchanged Ubuntu
Core certificate-counter test and dependent jobs passed on the one-time
retry ([run 34117304035, attempt
2](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/34117304035/attempts/2))
- `Opc.Ua.XRegistry.Tests` — 327 passed on net10.0 and net48 for
migration commit `32c914aaf`
- `Opc.Ua.WotCon.Tests` net10.0 — 1348 passed
- `Opc.Ua.WotCon.Bindings.Tests` net10.0 — 683 passed
- `Opc.Ua.SourceGeneration.Core.Tests` net10.0 — 3807 passed, 8 skipped
- `Opc.Ua.SourceGeneration.Tests` net10.0 — 145 passed
- `Opc.Ua.SourceGeneration.Stack.Tests` net10.0 — 94 passed
- affected XRegistry and WoT client/server projects build successfully
- xRegistry NodeSet is byte-identical to the companion artifact
- repository-wide CA1861 inventory — 198 unique diagnostics across 54
files reduced to zero
- targeted changed-fixture validation — 1,098 passed, 3 skipped, 0
failed
- constrained full `UA.slnx` net10.0 Release build — CA1861 = 0
- `Opc.Ua.XRegistry.Client` net472 and netstandard2.1 builds — 0
warnings, 0 errors
- independent shared-array safety review — no significant issues
- `git diff --check`

The final review regressions cover concurrent reconcile/open, synthetic
logical
handle collisions across default switches, cross-session Close
isolation,
shared writer reservations, logical FileType property/change
propagation,
logical Labels delegation, role-fixed deletion, event sources,
model-version
gating, and stable `HasWoTProjection` targets.

The companion specification and protocol binding are reviewed in the
linked PRs above.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5c79a1ed-6dab-4522-a9fc-6548caa78ad5
Copilot-Session: b605797f-a97a-4cd0-8f12-016d33c2fd88
Copilot-Session: 7ef369a5-2307-4f71-8a11-15af0e72f240
Copilot-Session: e716d455-8397-4d22-9c5a-398847ab7317
2026-09-08 04:46:40 +00:00

237 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;
using System.Linq;
using NUnit.Framework;
using Opc.Ua;
using Opc.Ua.OpenUsd.Scene;
namespace Opc.Ua.OpenUsd.Tests
{
[TestFixture]
public class UsdaReaderPlantTests
{
private UsdStage _stage = null!;
[OneTimeSetUp]
public void LoadStage()
{
_stage = TestAssets.Load("Plant.usda");
}
[Test]
public void StageMetadata_IsParsed()
{
Assert.That(_stage.DefaultPrim, Is.EqualTo("Plant"));
Assert.That(_stage.UpAxis, Is.EqualTo("Z"));
Assert.That(_stage.MetersPerUnit, Is.EqualTo(1.0));
Assert.That(_stage.RootLayerIdentifier, Is.EqualTo("Plant.usda"));
Assert.That(_stage.Documentation, Does.Contain("Base pump asset"));
}
[Test]
public void PrimCountAndPaths_MatchReference()
{
List<string> paths = _stage.AllPrims().Select(p => p.Path).OrderBy(p => p, System.StringComparer.Ordinal).ToList();
Assert.That(paths, Has.Count.EqualTo(12));
Assert.That(paths, Is.EqualTo(s_stringValues1));
}
[Test]
public void RootPrim_IsPlantXform()
{
Assert.That(_stage.RootPrims, Has.Count.EqualTo(1));
UsdPrim plant = _stage.RootPrims[0];
Assert.That(plant.Name, Is.EqualTo("Plant"));
Assert.That(plant.TypeName, Is.EqualTo("Xform"));
Assert.That(plant.Specifier, Is.EqualTo(UsdSpecifierEnum.Def));
}
[Test]
public void Pumps_IsScope()
{
UsdPrim pumps = UsdTestHelpers.RequirePrim(_stage, "/Plant/Pumps");
Assert.That(pumps.TypeName, Is.EqualTo("Scope"));
Assert.That(pumps.Specifier, Is.EqualTo(UsdSpecifierEnum.Def));
}
[Test]
public void P101_HasKindAndReconstructedInstanceArc()
{
UsdPrim p101 = UsdTestHelpers.RequirePrim(_stage, "/Plant/Pumps/P101");
Assert.That(p101.TypeName, Is.EqualTo("Xform"));
Assert.That(p101.Kind, Is.EqualTo(UsdPrimKindEnum.Component));
Assert.That(p101.Composition, Has.Count.EqualTo(2));
UsdCompositionArc reference = p101.Composition[0];
Assert.That(reference.ArcKind, Is.EqualTo(UsdArcKindEnum.Reference));
Assert.That(reference.AssetPath, Is.EqualTo("pump.usda"));
Assert.That(reference.PrimPath, Is.EqualTo("/Pump"));
Assert.That(reference.ListPosition, Is.EqualTo(UsdListOpTypeEnum.Append));
UsdCompositionArc instance = p101.Composition[1];
Assert.That(instance.ArcKind, Is.EqualTo(UsdArcKindEnum.Instance));
Assert.That(instance.AssetPath, Is.EqualTo("pump.usda"));
Assert.That(instance.PrimPath, Is.EqualTo("/Pump"));
Assert.That(instance.ListPosition, Is.EqualTo(UsdListOpTypeEnum.Append));
}
[Test]
public void Body_AttributeTypeNamesAndValues()
{
UsdPrim body = UsdTestHelpers.RequirePrim(_stage, "/Plant/Pumps/P101/Body");
Assert.That(body.TypeName, Is.EqualTo("Cylinder"));
UsdAttribute axis = UsdTestHelpers.RequireAttribute(body, "axis");
Assert.That(axis.TypeName, Is.EqualTo("token"));
Assert.That(axis.Variability, Is.EqualTo(UsdVariabilityEnum.Uniform));
UsdTestHelpers.AssertText(axis.Value, "Z");
UsdAttribute radius = UsdTestHelpers.RequireAttribute(body, "radius");
Assert.That(radius.TypeName, Is.EqualTo("double"));
UsdTestHelpers.AssertDouble(radius.Value, 0.5);
UsdAttribute color = UsdTestHelpers.RequireAttribute(body, "primvars:displayColor");
Assert.That(color.TypeName, Is.EqualTo("color3f[]"));
Assert.That(color.Value.TryGetArray(out ArrayOf<UsdValue> outer), Is.True);
Assert.That(outer, Has.Count.EqualTo(1));
Assert.That(outer[0].TryGetTuple(out ArrayOf<UsdValue> tuple), Is.True);
Assert.That(tuple.ToArray()!.Select(v => v.TryGetInteger(out long integer) ? integer : -1L).ToArray(),
Is.EqualTo(s_longValues1));
UsdAttribute order = UsdTestHelpers.RequireAttribute(body, "xformOpOrder");
Assert.That(order.TypeName, Is.EqualTo("token[]"));
Assert.That(order.Variability, Is.EqualTo(UsdVariabilityEnum.Uniform));
Assert.That(order.Value.TryGetArray(out ArrayOf<UsdValue> orderValues), Is.True);
Assert.That(orderValues.ToArray()!.Select(v => v.TryGetText(out string token) ? token : string.Empty).ToArray(),
Is.EqualTo(s_stringValues2));
}
[Test]
public void Impeller_LiveAndCustomAttributes()
{
UsdPrim impeller = UsdTestHelpers.RequirePrim(_stage, "/Plant/Pumps/P101/Impeller");
UsdAttribute rotateZ = UsdTestHelpers.RequireAttribute(impeller, "xformOp:rotateZ");
Assert.That(rotateZ.TypeName, Is.EqualTo("double"));
Assert.That(rotateZ.Variability, Is.EqualTo(UsdVariabilityEnum.Varying));
Assert.That(rotateZ.Live, Is.True);
UsdTestHelpers.AssertInteger(rotateZ.Value, 0L);
UsdAttribute setpoint = UsdTestHelpers.RequireAttribute(impeller, "inputs:speedSetpoint");
Assert.That(setpoint.Custom, Is.True);
Assert.That(setpoint.TypeName, Is.EqualTo("double"));
UsdAttribute order = UsdTestHelpers.RequireAttribute(impeller, "xformOpOrder");
Assert.That(order.Value.TryGetArray(out ArrayOf<UsdValue> orderValues), Is.True);
Assert.That(orderValues.ToArray()!.Select(v => v.TryGetText(out string token) ? token : string.Empty).ToArray(),
Is.EqualTo(s_stringValues3));
}
[Test]
public void Bulb_HasAppliedApiSchemaAndRelationship()
{
UsdPrim bulb = UsdTestHelpers.RequirePrim(_stage, "/Plant/Pumps/P101/StatusLight/Bulb");
Assert.That(bulb.TypeName, Is.EqualTo("Sphere"));
Assert.That(bulb.ApiSchemas.Select(s => s.SchemaName), Is.EqualTo(s_stringValues4));
UsdRelationship binding = UsdTestHelpers.RequireRelationship(bulb, "material:binding");
Assert.That(binding.Targets, Is.EqualTo(s_stringValues5));
}
[Test]
public void Material_ConnectionIsParsed()
{
UsdPrim mat = UsdTestHelpers.RequirePrim(_stage, "/Plant/Pumps/P101/StatusLight/Mat");
Assert.That(mat.TypeName, Is.EqualTo("Material"));
UsdAttribute surface = UsdTestHelpers.RequireAttribute(mat, "outputs:surface");
Assert.That(surface.TypeName, Is.EqualTo("token"));
Assert.That(surface.Value.IsNull, Is.True);
Assert.That(surface.Connections, Is.EqualTo(s_stringValues6));
}
[Test]
public void Shader_ScalarTupleAndUnvaluedAttributes()
{
UsdPrim shader = UsdTestHelpers.RequirePrim(_stage, "/Plant/Pumps/P101/StatusLight/Mat/Surface");
Assert.That(shader.TypeName, Is.EqualTo("Shader"));
UsdAttribute id = UsdTestHelpers.RequireAttribute(shader, "info:id");
Assert.That(id.TypeName, Is.EqualTo("token"));
Assert.That(id.Variability, Is.EqualTo(UsdVariabilityEnum.Uniform));
UsdTestHelpers.AssertText(id.Value, "UsdPreviewSurface");
UsdAttribute diffuse = UsdTestHelpers.RequireAttribute(shader, "inputs:diffuseColor");
Assert.That(diffuse.TypeName, Is.EqualTo("color3f"));
Assert.That(diffuse.Value.TryGetTuple(out ArrayOf<UsdValue> diffuseValues), Is.True);
Assert.That(diffuseValues.ToArray()!.Select(v => v.TryGetDouble(out double d) ? d : double.NaN).ToArray(),
Is.EqualTo(s_doubleValues1));
UsdAttribute emissive = UsdTestHelpers.RequireAttribute(shader, "inputs:emissiveColor");
Assert.That(emissive.Value.TryGetTuple(out ArrayOf<UsdValue> emissiveValues), Is.True);
Assert.That(emissiveValues.ToArray()!.Select(v => v.TryGetInteger(out long integer) ? integer : -1L).ToArray(),
Is.EqualTo(s_longValues2));
UsdAttribute outputs = UsdTestHelpers.RequireAttribute(shader, "outputs:surface");
Assert.That(outputs.Value.IsNull, Is.True);
Assert.That(outputs.Connections, Is.Empty);
}
private static readonly string[] s_stringValues1 =
[
"/Plant",
"/Plant/Pumps",
"/Plant/Pumps/P101",
"/Plant/Pumps/P101/Bearing",
"/Plant/Pumps/P101/Body",
"/Plant/Pumps/P101/Impeller",
"/Plant/Pumps/P101/Impeller/BladeA",
"/Plant/Pumps/P101/Impeller/BladeB",
"/Plant/Pumps/P101/StatusLight",
"/Plant/Pumps/P101/StatusLight/Bulb",
"/Plant/Pumps/P101/StatusLight/Mat",
"/Plant/Pumps/P101/StatusLight/Mat/Surface",
];
private static readonly long[] s_longValues1 = [0L, 0L, 1L];
private static readonly string[] s_stringValues2 = ["xformOp:translate"];
private static readonly string[] s_stringValues3 = ["xformOp:translate", "xformOp:rotateZ"];
private static readonly string[] s_stringValues4 = ["MaterialBindingAPI"];
private static readonly string[] s_stringValues5 = ["/Plant/Pumps/P101/StatusLight/Mat"];
private static readonly string[] s_stringValues6 =
[
"/Plant/Pumps/P101/StatusLight/Mat/Surface.outputs:surface",
];
private static readonly double[] s_doubleValues1 = [0.1, 0.1, 0.1];
private static readonly long[] s_longValues2 = [0L, 0L, 0L];
}
}