1f43449942
Browse device… on a /raw Device/TagGroup opens the discovery-browser modal against the merged Driver+Device config and commits selected browse leaves as raw Tag rows under the target, via IRawTreeService.ImportTagsAsync. - RawBrowseModal.razor: two-tier browse gate (bespoke IDriverBrowser → universal DiscoveryDriverBrowser via SupportsOnlineDiscovery → disabled), merged config from LoadMergedProbeConfigAsync, multi-select over DriverBrowseTree, opt-in 'create matching tag-groups' folder mirror, commit via ImportTagsAsync. - RawBrowseCommitMapper: pure leaf→RawTagImportRow mapper — DriverDataType→ OPC UA type, per-driver address field (nodeId/tagPath/symbolPath/address/ attributeRef), target-group + mirror path combine. Unit-tested (39 tests). - DriverBrowseTree: additive multi-select mode (checkboxes + ancestor-path callback), default off preserves single-select pickers. - RawTree: OnBrowseDevice wired to the modal (Device + TagGroup menus). Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
202 lines
7.7 KiB
C#
202 lines
7.7 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
|
|
|
/// <summary>
|
|
/// Unit tests for <see cref="RawBrowseCommitMapper"/> — the WP6 browse-leaf → <see cref="RawTagImportRow"/>
|
|
/// mapper. Covers the driver data-type mapping, the per-driver address-field placement in the driver-typed
|
|
/// <c>TagConfig</c>, and the target-group + folder-mirror path combine.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class RawBrowseCommitMapperTests
|
|
{
|
|
// ---- MapDataType ----------------------------------------------------------------------------
|
|
|
|
[Theory]
|
|
[InlineData("Boolean", "Boolean")]
|
|
[InlineData("Int16", "Int16")]
|
|
[InlineData("Int32", "Int32")]
|
|
[InlineData("Int64", "Int64")]
|
|
[InlineData("UInt16", "UInt16")]
|
|
[InlineData("UInt32", "UInt32")]
|
|
[InlineData("UInt64", "UInt64")]
|
|
[InlineData("Float32", "Float")]
|
|
[InlineData("Float64", "Double")]
|
|
[InlineData("String", "String")]
|
|
[InlineData("DateTime", "DateTime")]
|
|
[InlineData("Reference", "String")]
|
|
public void MapDataType_maps_every_driver_data_type_to_its_opcua_builtin_name(string driverType, string expected)
|
|
=> RawBrowseCommitMapper.MapDataType(driverType).ShouldBe(expected);
|
|
|
|
[Fact]
|
|
public void MapDataType_is_case_insensitive()
|
|
=> RawBrowseCommitMapper.MapDataType("float32").ShouldBe("Float");
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
[InlineData("NotAType")]
|
|
public void MapDataType_returns_null_for_blank_or_unknown(string? input)
|
|
=> RawBrowseCommitMapper.MapDataType(input).ShouldBeNull();
|
|
|
|
[Fact]
|
|
public void MapDataType_covers_the_whole_DriverDataType_enum()
|
|
{
|
|
// Guard: if a new DriverDataType is added, this fails until MapDataType handles it.
|
|
foreach (var dt in Enum.GetValues<DriverDataType>())
|
|
RawBrowseCommitMapper.MapDataType(dt.ToString()).ShouldNotBeNull(
|
|
$"DriverDataType.{dt} has no OPC-UA type mapping.");
|
|
}
|
|
|
|
// ---- BuildTagConfig (address field per driver) ----------------------------------------------
|
|
|
|
[Theory]
|
|
[InlineData("OpcUaClient", "nodeId", "ns=2;s=Channel1.Device1.Tag1")]
|
|
[InlineData("AbCip", "tagPath", "Program:Main.Motor.Speed")]
|
|
[InlineData("AbLegacy", "address", "N7:0")]
|
|
[InlineData("TwinCAT", "symbolPath", "MAIN.bStart")]
|
|
[InlineData("FOCAS", "address", "R100")]
|
|
[InlineData("S7", "address", "DB1.DBW0")]
|
|
[InlineData("GalaxyMxGateway", "attributeRef", "Tank_001.Level")]
|
|
public void BuildTagConfig_writes_the_reference_into_the_drivers_address_field(
|
|
string driverType, string expectedKey, string reference)
|
|
{
|
|
var json = RawBrowseCommitMapper.BuildTagConfig(driverType, reference);
|
|
|
|
var o = JsonNode.Parse(json)!.AsObject();
|
|
o[expectedKey]!.GetValue<string>().ShouldBe(reference);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildTagConfig_is_case_insensitive_on_driver_type()
|
|
{
|
|
var json = RawBrowseCommitMapper.BuildTagConfig("opcuaclient", "ns=2;s=X");
|
|
JsonNode.Parse(json)!.AsObject()["nodeId"]!.GetValue<string>().ShouldBe("ns=2;s=X");
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildTagConfig_does_not_write_any_identity_key()
|
|
{
|
|
// v3: identity is the RawPath — TagConfig must NOT carry a FullName identity key.
|
|
var json = RawBrowseCommitMapper.BuildTagConfig("OpcUaClient", "ns=2;s=X");
|
|
JsonNode.Parse(json)!.AsObject().ContainsKey("FullName").ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildTagConfig_unknown_driver_falls_back_to_a_generic_address_key()
|
|
{
|
|
var json = RawBrowseCommitMapper.BuildTagConfig("Modbus", "40001");
|
|
JsonNode.Parse(json)!.AsObject()["address"]!.GetValue<string>().ShouldBe("40001");
|
|
}
|
|
|
|
// ---- CombineGroupPath -----------------------------------------------------------------------
|
|
|
|
[Theory]
|
|
[InlineData(null, null, null)]
|
|
[InlineData("Grp", null, "Grp")]
|
|
[InlineData(null, "Sub", "Sub")]
|
|
[InlineData("Grp", "Sub", "Grp/Sub")]
|
|
[InlineData("Grp/Deep", "Sub/Leaf", "Grp/Deep/Sub/Leaf")]
|
|
[InlineData(" ", "Sub", "Sub")]
|
|
public void CombineGroupPath_joins_prefix_and_suffix(string? prefix, string? suffix, string? expected)
|
|
=> RawBrowseCommitMapper.CombineGroupPath(prefix, suffix).ShouldBe(expected);
|
|
|
|
// ---- MapLeaf (end to end) -------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void MapLeaf_builds_a_read_row_with_typed_config_and_no_group_at_device_root()
|
|
{
|
|
var row = RawBrowseCommitMapper.MapLeaf(
|
|
driverType: "TwinCAT",
|
|
fullName: "MAIN.rSpeed",
|
|
browseName: "rSpeed",
|
|
driverDataType: "Float32",
|
|
defaultDataType: "Double",
|
|
groupPrefix: null,
|
|
folderPath: new[] { "MAIN" },
|
|
createGroups: false);
|
|
|
|
row.TagGroupPath.ShouldBeNull(); // mirror OFF, no target group
|
|
row.Tag.Name.ShouldBe("rSpeed");
|
|
row.Tag.DataType.ShouldBe("Float"); // Float32 -> Float
|
|
row.Tag.AccessLevel.ShouldBe(TagAccessLevel.Read);
|
|
row.Tag.WriteIdempotent.ShouldBeFalse();
|
|
row.Tag.PollGroupId.ShouldBeNull();
|
|
JsonNode.Parse(row.Tag.TagConfig)!.AsObject()["symbolPath"]!.GetValue<string>().ShouldBe("MAIN.rSpeed");
|
|
}
|
|
|
|
[Fact]
|
|
public void MapLeaf_mirrors_folder_path_onto_group_when_create_groups_on()
|
|
{
|
|
var row = RawBrowseCommitMapper.MapLeaf(
|
|
driverType: "AbCip",
|
|
fullName: "Program:Main.Motor.Speed",
|
|
browseName: "Speed",
|
|
driverDataType: "Int32",
|
|
defaultDataType: "Double",
|
|
groupPrefix: null,
|
|
folderPath: new[] { "Program:Main", "Motor" },
|
|
createGroups: true);
|
|
|
|
row.TagGroupPath.ShouldBe("Program:Main/Motor");
|
|
}
|
|
|
|
[Fact]
|
|
public void MapLeaf_prepends_target_group_prefix_and_mirror()
|
|
{
|
|
var row = RawBrowseCommitMapper.MapLeaf(
|
|
driverType: "AbCip",
|
|
fullName: "Program:Main.Motor.Speed",
|
|
browseName: "Speed",
|
|
driverDataType: "Int32",
|
|
defaultDataType: "Double",
|
|
groupPrefix: "Imported",
|
|
folderPath: new[] { "Motor" },
|
|
createGroups: true);
|
|
|
|
row.TagGroupPath.ShouldBe("Imported/Motor");
|
|
}
|
|
|
|
[Fact]
|
|
public void MapLeaf_uses_target_group_prefix_only_when_mirror_off()
|
|
{
|
|
var row = RawBrowseCommitMapper.MapLeaf(
|
|
driverType: "AbCip",
|
|
fullName: "Program:Main.Motor.Speed",
|
|
browseName: "Speed",
|
|
driverDataType: "Int32",
|
|
defaultDataType: "Double",
|
|
groupPrefix: "Imported",
|
|
folderPath: new[] { "Motor" },
|
|
createGroups: false);
|
|
|
|
row.TagGroupPath.ShouldBe("Imported");
|
|
}
|
|
|
|
[Fact]
|
|
public void MapLeaf_falls_back_to_default_data_type_when_browser_reports_none()
|
|
{
|
|
// OPC UA Client browse reports no attribute type — the default fills in.
|
|
var row = RawBrowseCommitMapper.MapLeaf(
|
|
driverType: "OpcUaClient",
|
|
fullName: "ns=2;s=Tag1",
|
|
browseName: "Tag1",
|
|
driverDataType: null,
|
|
defaultDataType: "Double",
|
|
groupPrefix: null,
|
|
folderPath: null,
|
|
createGroups: true); // mirror ON but null folderPath -> still device root
|
|
|
|
row.TagGroupPath.ShouldBeNull();
|
|
row.Tag.DataType.ShouldBe("Double");
|
|
JsonNode.Parse(row.Tag.TagConfig)!.AsObject()["nodeId"]!.GetValue<string>().ShouldBe("ns=2;s=Tag1");
|
|
}
|
|
}
|