Auto: opcuaclient-8 — type definition mirroring
Adds an opt-in pass-3 walk of the upstream TypesFolder (i=86) so the OPC UA Client driver can mirror upstream type definitions into the local address space. Honours the curation rules from PR-7 (#359). Structural mirror only — binary-encoding priming via LoadDataTypeSystem is tracked as a follow-up because that helper was removed from the public ISession surface in OPCFoundation.NetStandard 1.5.378+. - IAddressSpaceBuilder.RegisterTypeNode (default no-op for back-compat) - MirroredTypeNodeInfo + MirroredTypeKind in Core.Abstractions - OpcUaClientDriverOptions.MirrorTypeDefinitions (default false) - DiscoverAsync pass-3: FetchTypeTreeAsync + recursive HasSubtype walk per branch (ObjectTypes/VariableTypes/DataTypes/ReferenceTypes), best-effort IsAbstract read, IncludePaths/ExcludePaths still applied - 6 new unit tests; all 153 OpcUaClient unit tests pass Closes #280
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the type-mirror surface added in PR-8 (Issue #280). Live-browse
|
||||
/// coverage of the Pass-3 walk against an upstream server lands in integration tests
|
||||
/// against opc-plc; these unit assertions cover the option default, the new
|
||||
/// <see cref="MirroredTypeNodeInfo"/> shape, and the back-compat default no-op for
|
||||
/// <see cref="IAddressSpaceBuilder.RegisterTypeNode"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class OpcUaClientTypeMirrorTests
|
||||
{
|
||||
[Fact]
|
||||
public void MirrorTypeDefinitions_default_is_false_so_existing_deployments_are_unchanged()
|
||||
{
|
||||
var opts = new OpcUaClientDriverOptions();
|
||||
opts.MirrorTypeDefinitions.ShouldBeFalse(
|
||||
"opt-in flag — existing deployments shouldn't suddenly see a flood of type nodes");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MirrorTypeDefinitions_can_be_enabled_via_init()
|
||||
{
|
||||
var opts = new OpcUaClientDriverOptions { MirrorTypeDefinitions = true };
|
||||
opts.MirrorTypeDefinitions.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MirroredTypeNodeInfo_carries_kind_identity_supertype_and_abstract_flag()
|
||||
{
|
||||
var info = new MirroredTypeNodeInfo(
|
||||
Kind: MirroredTypeKind.ObjectType,
|
||||
UpstreamNodeId: "ns=2;i=1234",
|
||||
BrowseName: "MyMachine",
|
||||
DisplayName: "My Machine",
|
||||
SuperTypeNodeId: "i=58", // BaseObjectType
|
||||
IsAbstract: false);
|
||||
|
||||
info.Kind.ShouldBe(MirroredTypeKind.ObjectType);
|
||||
info.UpstreamNodeId.ShouldBe("ns=2;i=1234");
|
||||
info.BrowseName.ShouldBe("MyMachine");
|
||||
info.DisplayName.ShouldBe("My Machine");
|
||||
info.SuperTypeNodeId.ShouldBe("i=58");
|
||||
info.IsAbstract.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MirroredTypeKind_covers_all_four_OPC_UA_type_categories()
|
||||
{
|
||||
// The four standard OPC UA type-folder children — ObjectTypes (i=88),
|
||||
// VariableTypes (i=89), DataTypes (i=90), ReferenceTypes (i=91). Asserts the enum
|
||||
// shape rather than the values so the test catches accidental category renames.
|
||||
Enum.GetValues<MirroredTypeKind>().Length.ShouldBe(4);
|
||||
Enum.IsDefined(MirroredTypeKind.ObjectType).ShouldBeTrue();
|
||||
Enum.IsDefined(MirroredTypeKind.VariableType).ShouldBeTrue();
|
||||
Enum.IsDefined(MirroredTypeKind.DataType).ShouldBeTrue();
|
||||
Enum.IsDefined(MirroredTypeKind.ReferenceType).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterTypeNode_default_implementation_is_no_op_so_existing_builders_dont_break()
|
||||
{
|
||||
// Builders that only implement Folder + Variable + AddProperty (i.e. the pre-PR-8
|
||||
// contract) should continue to work — the default no-op on RegisterTypeNode means
|
||||
// calling it on a minimal builder doesn't throw.
|
||||
IAddressSpaceBuilder builder = new MinimalBuilder();
|
||||
var info = new MirroredTypeNodeInfo(MirroredTypeKind.DataType,
|
||||
UpstreamNodeId: "i=290", BrowseName: "Duration", DisplayName: "Duration",
|
||||
SuperTypeNodeId: "i=290", IsAbstract: false);
|
||||
|
||||
Should.NotThrow(() => builder.RegisterTypeNode(info));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterTypeNode_can_be_overridden_so_OPC_UA_server_builders_record_the_call()
|
||||
{
|
||||
// The OPC UA server-side DriverNodeManager will override RegisterTypeNode to actually
|
||||
// project the type into the local address space. This test models that contract with
|
||||
// a recording stub.
|
||||
var recorder = new RecordingTypeBuilder();
|
||||
IAddressSpaceBuilder builder = recorder;
|
||||
|
||||
builder.RegisterTypeNode(new MirroredTypeNodeInfo(
|
||||
MirroredTypeKind.ObjectType, "ns=2;i=1", "T1", "T1", null, false));
|
||||
builder.RegisterTypeNode(new MirroredTypeNodeInfo(
|
||||
MirroredTypeKind.VariableType, "ns=2;i=2", "V1", "V1", "i=63", true));
|
||||
|
||||
recorder.Calls.Count.ShouldBe(2);
|
||||
recorder.Calls[0].Kind.ShouldBe(MirroredTypeKind.ObjectType);
|
||||
recorder.Calls[1].IsAbstract.ShouldBeTrue();
|
||||
recorder.Calls[1].SuperTypeNodeId.ShouldBe("i=63");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimum back-compat implementation — only the pre-PR-8 surface is overridden.
|
||||
/// Verifies the default-implementation no-op for <c>RegisterTypeNode</c> doesn't break
|
||||
/// existing builders.
|
||||
/// </summary>
|
||||
private sealed class MinimalBuilder : IAddressSpaceBuilder
|
||||
{
|
||||
public IAddressSpaceBuilder Folder(string browseName, string displayName) => this;
|
||||
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
|
||||
=> new StubHandle();
|
||||
public void AddProperty(string browseName, DriverDataType dataType, object? value) { }
|
||||
|
||||
private sealed class StubHandle : IVariableHandle
|
||||
{
|
||||
public string FullReference => "stub";
|
||||
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingTypeBuilder : IAddressSpaceBuilder
|
||||
{
|
||||
public List<MirroredTypeNodeInfo> Calls { get; } = new();
|
||||
public IAddressSpaceBuilder Folder(string browseName, string displayName) => this;
|
||||
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
|
||||
=> throw new NotSupportedException();
|
||||
public void AddProperty(string browseName, DriverDataType dataType, object? value) { }
|
||||
public void RegisterTypeNode(MirroredTypeNodeInfo info) => Calls.Add(info);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user