Adds OpcUaClientCurationOptions on OpcUaClientDriverOptions.Curation with: - IncludePaths/ExcludePaths globs (* and ? only) matched against the slash-joined BrowsePath segments collected during BrowseRecursiveAsync. Empty Include = include all so existing deployments are unaffected; Exclude wins over Include. Pruned folders are skipped wholesale, so descendants don't reach the wire. - NamespaceRemap (URI -> URI) applied to DriverAttributeInfo.FullName when registering variables. Index-0 / standard nodes round-trip unchanged; remapped nodes serialise via ExpandedNodeId so downstream clients see the local-side URI. - RootAlias replaces the hard-coded "Remote" folder name at the top of the mirrored tree. Closes #279
156 lines
6.0 KiB
C#
156 lines
6.0 KiB
C#
using Opc.Ua;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for the curation surface added in PR-7 (selective import + namespace
|
|
/// remap + root-alias). Focused on the pure-function helpers
|
|
/// (<see cref="OpcUaClientDriver.CompileGlobs"/>,
|
|
/// <see cref="OpcUaClientDriver.ShouldInclude"/>,
|
|
/// <see cref="OpcUaClientDriver.BuildRemappedFullName"/>) so the assertions don't need a
|
|
/// live upstream server. End-to-end browse + filter coverage lands in the integration
|
|
/// tests against opc-plc.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class OpcUaClientCurationTests
|
|
{
|
|
[Fact]
|
|
public void CurationOptions_default_is_empty_so_existing_behaviour_is_preserved()
|
|
{
|
|
var opts = new OpcUaClientDriverOptions();
|
|
opts.Curation.IncludePaths.ShouldBeNull();
|
|
opts.Curation.ExcludePaths.ShouldBeNull();
|
|
opts.Curation.NamespaceRemap.ShouldBeNull();
|
|
opts.Curation.RootAlias.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void ShouldInclude_empty_include_means_include_all()
|
|
{
|
|
OpcUaClientDriver.ShouldInclude("Server/Status", include: null, exclude: null).ShouldBeTrue();
|
|
OpcUaClientDriver.ShouldInclude("anything", include: null, exclude: null).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void ShouldInclude_star_glob_matches_any_segment_chars()
|
|
{
|
|
var include = OpcUaClientDriver.CompileGlobs(new[] { "Plant/*" });
|
|
OpcUaClientDriver.ShouldInclude("Plant/Tank1", include, null).ShouldBeTrue();
|
|
OpcUaClientDriver.ShouldInclude("Plant/Tank1/Level", include, null).ShouldBeTrue("'*' matches across slashes — single-glob simple semantics");
|
|
OpcUaClientDriver.ShouldInclude("Other/Tank1", include, null).ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void ShouldInclude_question_mark_matches_exactly_one_char()
|
|
{
|
|
var include = OpcUaClientDriver.CompileGlobs(new[] { "Tank?" });
|
|
OpcUaClientDriver.ShouldInclude("Tank1", include, null).ShouldBeTrue();
|
|
OpcUaClientDriver.ShouldInclude("Tank12", include, null).ShouldBeFalse();
|
|
OpcUaClientDriver.ShouldInclude("Tank", include, null).ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void ShouldInclude_exclude_wins_over_include()
|
|
{
|
|
var include = OpcUaClientDriver.CompileGlobs(new[] { "Plant/*" });
|
|
var exclude = OpcUaClientDriver.CompileGlobs(new[] { "Plant/Diagnostics*" });
|
|
OpcUaClientDriver.ShouldInclude("Plant/Tank1", include, exclude).ShouldBeTrue();
|
|
OpcUaClientDriver.ShouldInclude("Plant/Diagnostics", include, exclude).ShouldBeFalse();
|
|
OpcUaClientDriver.ShouldInclude("Plant/DiagnosticsTree/Sub", include, exclude).ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void ShouldInclude_multiple_includes_are_OR()
|
|
{
|
|
var include = OpcUaClientDriver.CompileGlobs(new[] { "PlantA/*", "PlantB/*" });
|
|
OpcUaClientDriver.ShouldInclude("PlantA/Tank", include, null).ShouldBeTrue();
|
|
OpcUaClientDriver.ShouldInclude("PlantB/Tank", include, null).ShouldBeTrue();
|
|
OpcUaClientDriver.ShouldInclude("PlantC/Tank", include, null).ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void CompileGlobs_returns_null_for_empty_or_null_input()
|
|
{
|
|
OpcUaClientDriver.CompileGlobs(null).ShouldBeNull();
|
|
OpcUaClientDriver.CompileGlobs(System.Array.Empty<string>()).ShouldBeNull();
|
|
OpcUaClientDriver.CompileGlobs(new[] { string.Empty }).ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void CompileGlobs_escapes_regex_metacharacters_in_literal_segments()
|
|
{
|
|
// A bare "." in the glob must NOT match arbitrary chars — it's escaped to a literal dot.
|
|
var rx = OpcUaClientDriver.CompileGlobs(new[] { "App.exe" });
|
|
rx.ShouldNotBeNull();
|
|
rx!.IsMatch("App.exe").ShouldBeTrue();
|
|
rx.IsMatch("AppXexe").ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildRemappedFullName_returns_default_string_when_remap_is_empty()
|
|
{
|
|
var node = new NodeId("Tank/Level", 2);
|
|
OpcUaClientDriver.BuildRemappedFullName(node, table: null, remap: null)
|
|
.ShouldBe(node.ToString());
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildRemappedFullName_passes_through_namespace_zero_unchanged()
|
|
{
|
|
// Standard OPC UA nodes (ns=0) bypass remap so the base namespace round-trips.
|
|
var node = new NodeId(2253u); // Server
|
|
var remap = new Dictionary<string, string> { ["http://opcfoundation.org/UA/"] = "urn:local:base" };
|
|
OpcUaClientDriver.BuildRemappedFullName(node, table: null, remap: remap)
|
|
.ShouldBe(node.ToString());
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildRemappedFullName_translates_upstream_uri_when_remap_hits()
|
|
{
|
|
var table = new NamespaceTable();
|
|
table.Append("urn:upstream:plc-vendor"); // index 1
|
|
var node = new NodeId("Tank/Level", 1);
|
|
var remap = new Dictionary<string, string>
|
|
{
|
|
["urn:upstream:plc-vendor"] = "urn:local:plant",
|
|
};
|
|
|
|
var result = OpcUaClientDriver.BuildRemappedFullName(node, table, remap);
|
|
|
|
result.ShouldContain("urn:local:plant", Case.Sensitive);
|
|
result.ShouldNotContain("urn:upstream:plc-vendor");
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildRemappedFullName_no_op_when_upstream_uri_not_in_remap_table()
|
|
{
|
|
var table = new NamespaceTable();
|
|
table.Append("urn:upstream:other"); // index 1
|
|
var node = new NodeId("Some/Item", 1);
|
|
var remap = new Dictionary<string, string>
|
|
{
|
|
["urn:not-matching"] = "urn:local:something",
|
|
};
|
|
OpcUaClientDriver.BuildRemappedFullName(node, table, remap).ShouldBe(node.ToString());
|
|
}
|
|
|
|
[Fact]
|
|
public void RootAlias_default_null_means_Remote()
|
|
{
|
|
var opts = new OpcUaClientDriverOptions();
|
|
opts.Curation.RootAlias.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void RootAlias_can_be_set_via_Curation_record()
|
|
{
|
|
var opts = new OpcUaClientDriverOptions
|
|
{
|
|
Curation = new OpcUaClientCurationOptions(RootAlias: "Plant"),
|
|
};
|
|
opts.Curation.RootAlias.ShouldBe("Plant");
|
|
}
|
|
}
|