v3(b1-twincat): apply Modbus exemplar pattern to TwinCAT (multi-device)
- Rename TwinCATEquipmentTagParser -> TwinCATTagDefinitionFactory; TryParse ->
FromTagConfig(tagConfig, rawPath, out def). Drop leading-'{' heuristic + the
deviceHostAddress key; def.Name = rawPath. Keep strict-enum + Inspect; add a
Structure guard (sole tag path now) + inverse ToTagConfig.
- Options: Tags -> RawTags (RawTagEntry); keep Devices + protocol fields.
- Driver: _tagsByRawPath (Ordinal); byName-only resolver; build table from
_options.RawTags via FromTagConfig, threading WriteIdempotent + DeviceName.
Multi-device: ResolveDeviceHost matches entry.DeviceName against options.Devices
(by name, then host) -> def.DeviceHostAddress (TODO(v3 WaveC): live host from the
Device row's DeviceConfig). DiscoverAsync now emits from the table.
- Factory: retire pre-declared tags[] DTO path; bind rawTags; keep Devices.
- Cli: BuildOptions serialises typed defs -> RawTagEntry via ToTagConfig.
- Tests: TwinCATRawTags helper; migrate Tags= -> RawTags=; rename parser tests to
FromTagConfig; equipment/resolve-host/array tests delivered via RawTags.
Gate: Contracts + Driver build green; Driver.TwinCAT.Tests 192 pass, Cli.Tests 70 pass.
This commit is contained in:
+10
-1
@@ -99,7 +99,16 @@ public sealed class TwinCATCommandBaseTests
|
||||
options.Devices.Count.ShouldBe(1);
|
||||
options.Devices[0].HostAddress.ShouldBe("ads://10.0.0.1.1.1:851");
|
||||
options.Devices[0].DeviceName.ShouldBe("cli-10.0.0.1.1.1:851");
|
||||
options.Tags.ShouldBe([tag]);
|
||||
// v3: the CLI serialises its typed def into a RawTagEntry (RawPath = def.Name, DeviceName =
|
||||
// def.DeviceHostAddress) that round-trips through the factory back to the same def.
|
||||
options.RawTags.Count.ShouldBe(1);
|
||||
var entry = options.RawTags[0];
|
||||
entry.RawPath.ShouldBe("n1");
|
||||
entry.DeviceName.ShouldBe(cmd.GatewayForTest);
|
||||
TwinCAT.TwinCATTagDefinitionFactory.FromTagConfig(entry.TagConfig, entry.RawPath, out var roundTripped)
|
||||
.ShouldBeTrue();
|
||||
roundTripped.SymbolPath.ShouldBe("MAIN.x");
|
||||
roundTripped.DataType.ShouldBe(TwinCAT.TwinCATDataType.DInt);
|
||||
options.Timeout.ShouldBe(TimeSpan.FromMilliseconds(4321));
|
||||
options.Probe.Enabled.ShouldBeFalse();
|
||||
options.EnableControllerBrowse.ShouldBeFalse();
|
||||
|
||||
@@ -27,11 +27,11 @@ public sealed class TwinCATArraySupportTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(Host)],
|
||||
Tags =
|
||||
RawTags = TwinCATRawTags.Entries(
|
||||
[
|
||||
new TwinCATTagDefinition("Speeds", Host, "MAIN.Speeds", TwinCATDataType.DInt,
|
||||
Writable: true, WriteIdempotent: false, ArrayLength: 8),
|
||||
],
|
||||
]),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
EnableControllerBrowse = false,
|
||||
}, "drv-1", new FakeTwinCATClientFactory());
|
||||
@@ -52,7 +52,7 @@ public sealed class TwinCATArraySupportTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(Host)],
|
||||
Tags = [new TwinCATTagDefinition("Speed", Host, "MAIN.Speed", TwinCATDataType.DInt)],
|
||||
RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("Speed", Host, "MAIN.Speed", TwinCATDataType.DInt)]),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
}, "drv-1", new FakeTwinCATClientFactory());
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
@@ -104,12 +104,14 @@ public sealed class TwinCATArraySupportTests
|
||||
|
||||
// ---- (2) read path: array read returns a typed CLR array ----
|
||||
|
||||
/// <summary>An equipment array tag reads a typed CLR array (boxed as object) through the driver.</summary>
|
||||
private const string ArrayRawPath = "equip/Speeds";
|
||||
private const string ScalarRawPath = "equip/Speed";
|
||||
|
||||
/// <summary>An array raw tag reads a typed CLR array (boxed as object) through the driver.</summary>
|
||||
[Fact]
|
||||
public async Task Driver_reads_an_array_equipment_tag_as_typed_clr_array()
|
||||
public async Task Driver_reads_an_array_raw_tag_as_typed_clr_array()
|
||||
{
|
||||
var json =
|
||||
$$"""{"deviceHostAddress":"{{Host}}","symbolPath":"MAIN.Speeds","dataType":"DInt","isArray":true,"arrayLength":4}""";
|
||||
var blob = """{"symbolPath":"MAIN.Speeds","dataType":"DInt","isArray":true,"arrayLength":4}""";
|
||||
var expected = new[] { 10, 20, 30, 40 };
|
||||
var factory = new FakeTwinCATClientFactory
|
||||
{
|
||||
@@ -118,12 +120,12 @@ public sealed class TwinCATArraySupportTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(Host)],
|
||||
Tags = [],
|
||||
RawTags = [new RawTagEntry(ArrayRawPath, blob, WriteIdempotent: false, DeviceName: Host)],
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
}, "twincat-arr", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
var r = await drv.ReadAsync([json], CancellationToken.None);
|
||||
var r = await drv.ReadAsync([ArrayRawPath], CancellationToken.None);
|
||||
|
||||
r[0].StatusCode.ShouldBe(TwinCATStatusMapper.Good);
|
||||
r[0].Value.ShouldBeOfType<int[]>().ShouldBe(expected);
|
||||
@@ -131,11 +133,11 @@ public sealed class TwinCATArraySupportTests
|
||||
factory.Clients[0].LastReadArrayCount.ShouldBe(4);
|
||||
}
|
||||
|
||||
/// <summary>A scalar equipment tag reads with a null array count (scalar path unchanged).</summary>
|
||||
/// <summary>A scalar raw tag reads with a null array count (scalar path unchanged).</summary>
|
||||
[Fact]
|
||||
public async Task Driver_reads_a_scalar_equipment_tag_with_null_array_count()
|
||||
public async Task Driver_reads_a_scalar_raw_tag_with_null_array_count()
|
||||
{
|
||||
var json = $$"""{"deviceHostAddress":"{{Host}}","symbolPath":"MAIN.Speed","dataType":"DInt"}""";
|
||||
var blob = """{"symbolPath":"MAIN.Speed","dataType":"DInt"}""";
|
||||
var factory = new FakeTwinCATClientFactory
|
||||
{
|
||||
Customise = () => new FakeTwinCATClient { Values = { ["MAIN.Speed"] = 7 } },
|
||||
@@ -143,100 +145,83 @@ public sealed class TwinCATArraySupportTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(Host)],
|
||||
Tags = [],
|
||||
RawTags = [new RawTagEntry(ScalarRawPath, blob, WriteIdempotent: false, DeviceName: Host)],
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
}, "twincat-scalar", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
var r = await drv.ReadAsync([json], CancellationToken.None);
|
||||
var r = await drv.ReadAsync([ScalarRawPath], CancellationToken.None);
|
||||
|
||||
r[0].StatusCode.ShouldBe(TwinCATStatusMapper.Good);
|
||||
r[0].Value.ShouldBe(7);
|
||||
factory.Clients[0].LastReadArrayCount.ShouldBeNull();
|
||||
}
|
||||
|
||||
// ---- (3) resolver: equipment-tag parser threads arrayLength ----
|
||||
// ---- (3) mapper: FromTagConfig threads arrayLength ----
|
||||
|
||||
/// <summary>The equipment-tag parser threads <c>arrayLength</c> into the transient definition.</summary>
|
||||
/// <summary>The tag-definition factory threads <c>arrayLength</c> into the definition.</summary>
|
||||
[Fact]
|
||||
public void Parser_threads_arrayLength_into_the_definition()
|
||||
public void FromTagConfig_threads_arrayLength_into_the_definition()
|
||||
{
|
||||
var json =
|
||||
"""{"deviceHostAddress":"ads://5.23.91.23.1.1:851","symbolPath":"MAIN.Speeds","dataType":"DInt","isArray":true,"arrayLength":12}""";
|
||||
TwinCATEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
|
||||
var blob = """{"symbolPath":"MAIN.Speeds","dataType":"DInt","isArray":true,"arrayLength":12}""";
|
||||
TwinCATTagDefinitionFactory.FromTagConfig(blob, ArrayRawPath, out var def).ShouldBeTrue();
|
||||
def!.ArrayLength.ShouldBe(12);
|
||||
}
|
||||
|
||||
/// <summary>A blob without isArray (or with isArray=false) leaves ArrayLength null.</summary>
|
||||
[Fact]
|
||||
public void Parser_leaves_ArrayLength_null_for_a_scalar_blob()
|
||||
public void FromTagConfig_leaves_ArrayLength_null_for_a_scalar_blob()
|
||||
{
|
||||
TwinCATEquipmentTagParser.TryParse(
|
||||
"""{"symbolPath":"MAIN.X","dataType":"DInt"}""", out var def).ShouldBeTrue();
|
||||
TwinCATTagDefinitionFactory.FromTagConfig(
|
||||
"""{"symbolPath":"MAIN.X","dataType":"DInt"}""", ScalarRawPath, out var def).ShouldBeTrue();
|
||||
def!.ArrayLength.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>arrayLength is ignored when isArray is absent/false (no orphan length).</summary>
|
||||
[Fact]
|
||||
public void Parser_ignores_arrayLength_when_isArray_is_false()
|
||||
public void FromTagConfig_ignores_arrayLength_when_isArray_is_false()
|
||||
{
|
||||
TwinCATEquipmentTagParser.TryParse(
|
||||
TwinCATTagDefinitionFactory.FromTagConfig(
|
||||
"""{"symbolPath":"MAIN.X","dataType":"DInt","isArray":false,"arrayLength":9}""",
|
||||
out var def).ShouldBeTrue();
|
||||
ScalarRawPath, out var def).ShouldBeTrue();
|
||||
def!.ArrayLength.ShouldBeNull();
|
||||
}
|
||||
|
||||
// ---- (4) Driver.TwinCAT-017 — pre-declared array tag parseable via driver config JSON ----
|
||||
// ---- (4) Driver.TwinCAT-017 — array raw tag delivered via driver config JSON rawTags ----
|
||||
|
||||
/// <summary>
|
||||
/// A pre-declared tag in the driver config JSON with <c>arrayLength</c> must produce a
|
||||
/// An array raw tag in the driver config JSON <c>rawTags</c> array must bind + map to a
|
||||
/// <see cref="TwinCATTagDefinition"/> with the correct <see cref="TwinCATTagDefinition.ArrayLength"/>,
|
||||
/// which in turn drives <c>IsArray</c>/<c>ArrayDim</c> at discovery and a native array read
|
||||
/// at runtime (Driver.TwinCAT-017).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Predeclared_array_tag_arrayLength_parses_from_driver_config_json()
|
||||
public void Array_raw_tag_arrayLength_binds_from_driver_config_json()
|
||||
{
|
||||
var blob = """{"symbolPath":"MAIN.Speeds","dataType":"DInt","isArray":true,"arrayLength":8}""";
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(new
|
||||
{
|
||||
devices = new[] { new { hostAddress = Host } },
|
||||
tags = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
name = "Speeds",
|
||||
deviceHostAddress = Host,
|
||||
symbolPath = "MAIN.Speeds",
|
||||
dataType = "DInt",
|
||||
arrayLength = 8,
|
||||
},
|
||||
},
|
||||
rawTags = new[] { new { rawPath = "Speeds", tagConfig = blob, writeIdempotent = false, deviceName = Host } },
|
||||
});
|
||||
var parsed = TwinCATDriverFactoryExtensions.ParseOptionsForTests(json, "drv-1");
|
||||
parsed.Tags.Single().ArrayLength.ShouldBe(8);
|
||||
var entry = parsed.RawTags.Single();
|
||||
TwinCATTagDefinitionFactory.FromTagConfig(entry.TagConfig, entry.RawPath, out var def).ShouldBeTrue();
|
||||
def!.ArrayLength.ShouldBe(8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A pre-declared array tag parsed from JSON must surface as <c>IsArray=true</c> /
|
||||
/// An array raw tag delivered via the driver config JSON must surface as <c>IsArray=true</c> /
|
||||
/// <c>ArrayDim=8</c> in the discovery address space.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Predeclared_array_tag_from_json_reports_IsArray_in_discovery()
|
||||
public async Task Array_raw_tag_from_json_reports_IsArray_in_discovery()
|
||||
{
|
||||
var blob = """{"symbolPath":"MAIN.Speeds","dataType":"DInt","isArray":true,"arrayLength":8}""";
|
||||
var configJson = System.Text.Json.JsonSerializer.Serialize(new
|
||||
{
|
||||
devices = new[] { new { hostAddress = Host } },
|
||||
tags = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
name = "Speeds",
|
||||
deviceHostAddress = Host,
|
||||
symbolPath = "MAIN.Speeds",
|
||||
dataType = "DInt",
|
||||
arrayLength = 8,
|
||||
},
|
||||
},
|
||||
rawTags = new[] { new { rawPath = "Speeds", tagConfig = blob, writeIdempotent = false, deviceName = Host } },
|
||||
});
|
||||
var builder = new RecordingBuilder();
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions(), "drv-1", new FakeTwinCATClientFactory());
|
||||
|
||||
@@ -19,11 +19,11 @@ public sealed class TwinCATCapabilityTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851", DeviceName: "Mach1")],
|
||||
Tags =
|
||||
RawTags = TwinCATRawTags.Entries(
|
||||
[
|
||||
new TwinCATTagDefinition("Speed", "ads://5.23.91.23.1.1:851", "MAIN.Speed", TwinCATDataType.DInt),
|
||||
new TwinCATTagDefinition("Status", "ads://5.23.91.23.1.1:851", "GVL.Status", TwinCATDataType.Bool, Writable: false),
|
||||
],
|
||||
]),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
}, "drv-1");
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
@@ -49,7 +49,7 @@ public sealed class TwinCATCapabilityTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
|
||||
Tags = [new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt)],
|
||||
RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt)]),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
UseNativeNotifications = false, // poll-mode test
|
||||
}, "drv-1", factory);
|
||||
@@ -76,7 +76,7 @@ public sealed class TwinCATCapabilityTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
|
||||
Tags = [new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt)],
|
||||
RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt)]),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
UseNativeNotifications = false, // poll-mode test
|
||||
}, "drv-1", factory);
|
||||
@@ -198,11 +198,11 @@ public sealed class TwinCATCapabilityTests
|
||||
new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851"),
|
||||
new TwinCATDeviceOptions("ads://5.23.91.24.1.1:851"),
|
||||
],
|
||||
Tags =
|
||||
RawTags = TwinCATRawTags.Entries(
|
||||
[
|
||||
new TwinCATTagDefinition("A", "ads://5.23.91.23.1.1:851", "MAIN.A", TwinCATDataType.DInt),
|
||||
new TwinCATTagDefinition("B", "ads://5.23.91.24.1.1:851", "MAIN.B", TwinCATDataType.DInt),
|
||||
],
|
||||
]),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
}, "drv-1");
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
@@ -20,7 +20,7 @@ public sealed class TwinCATConnectBackoffTests
|
||||
private static TwinCATDriverOptions Options(bool probeEnabled) => new()
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(Host)],
|
||||
Tags = [new TwinCATTagDefinition("T", Host, "MAIN.T", TwinCATDataType.DInt)],
|
||||
RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("T", Host, "MAIN.T", TwinCATDataType.DInt)]),
|
||||
Probe = new TwinCATProbeOptions
|
||||
{
|
||||
Enabled = probeEnabled,
|
||||
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
|
||||
|
||||
/// <summary>R2-11 Phase C strictness surface for the TwinCAT equipment-tag parser: the runtime is now STRICT —
|
||||
/// a typo'd <c>dataType</c> rejects the tag (<c>TryParse</c> ⇒ <see langword="false"/> ⇒ <c>BadNodeIdUnknown</c>),
|
||||
/// <c>Inspect</c> reports the typo, and the <c>writable</c> key is honoured (default true).</summary>
|
||||
public sealed class TwinCATEquipmentTagParserStrictnessTests
|
||||
{
|
||||
[Fact]
|
||||
public void Typo_dataType_rejects_the_tag()
|
||||
{
|
||||
TwinCATEquipmentTagParser.TryParse("{\"symbolPath\":\"MAIN.x\",\"dataType\":\"DIntt\"}", out _)
|
||||
.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Valid_dataType_still_parses()
|
||||
{
|
||||
TwinCATEquipmentTagParser.TryParse("{\"symbolPath\":\"MAIN.x\",\"dataType\":\"DInt\"}", out var def)
|
||||
.ShouldBeTrue();
|
||||
def.DataType.ShouldBe(TwinCATDataType.DInt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Inspect_reports_typo_dataType()
|
||||
{
|
||||
var warnings = TwinCATEquipmentTagParser.Inspect("{\"symbolPath\":\"MAIN.x\",\"dataType\":\"DIntt\"}");
|
||||
warnings.ShouldHaveSingleItem();
|
||||
warnings[0].ShouldContain("DIntt");
|
||||
warnings[0].ShouldContain("dataType");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Inspect_clean_config_has_no_warnings()
|
||||
{
|
||||
TwinCATEquipmentTagParser.Inspect("{\"symbolPath\":\"MAIN.x\",\"dataType\":\"DInt\"}").ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Writable_false_is_honoured()
|
||||
{
|
||||
TwinCATEquipmentTagParser.TryParse("{\"symbolPath\":\"MAIN.x\",\"dataType\":\"DInt\",\"writable\":false}", out var def)
|
||||
.ShouldBeTrue();
|
||||
def.Writable.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Writable_absent_defaults_to_true()
|
||||
{
|
||||
TwinCATEquipmentTagParser.TryParse("{\"symbolPath\":\"MAIN.x\",\"dataType\":\"DInt\"}", out var def)
|
||||
.ShouldBeTrue();
|
||||
def.Writable.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public class TwinCATEquipmentTagTests
|
||||
{
|
||||
[Fact]
|
||||
public void Parses_equipment_tagconfig_into_a_transient_definition()
|
||||
{
|
||||
var json = """{"deviceHostAddress":"ads://5.23.91.23.1.1:851","symbolPath":"MAIN.Speed","dataType":"DInt"}""";
|
||||
TwinCATEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
|
||||
def!.Name.ShouldBe(json);
|
||||
def.SymbolPath.ShouldBe("MAIN.Speed");
|
||||
def.DeviceHostAddress.ShouldBe("ads://5.23.91.23.1.1:851");
|
||||
def.DataType.ShouldBe(TwinCATDataType.DInt);
|
||||
def.Writable.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Defaults_optional_fields_when_only_symbol_path_is_present()
|
||||
{
|
||||
TwinCATEquipmentTagParser.TryParse("""{"symbolPath":"GVL.X"}""", out var def).ShouldBeTrue();
|
||||
def!.SymbolPath.ShouldBe("GVL.X");
|
||||
def.DeviceHostAddress.ShouldBe(""); // absent host → empty
|
||||
def.DataType.ShouldBe(TwinCATDataType.DInt); // enum default
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rejects_a_non_address_blob()
|
||||
=> TwinCATEquipmentTagParser.TryParse("""{"FullName":"x"}""", out _).ShouldBeFalse();
|
||||
|
||||
[Fact]
|
||||
public void Rejects_garbage()
|
||||
=> TwinCATEquipmentTagParser.TryParse("not json", out _).ShouldBeFalse();
|
||||
|
||||
[Fact]
|
||||
public void Rejects_blank_reference()
|
||||
=> TwinCATEquipmentTagParser.TryParse(" ", out _).ShouldBeFalse();
|
||||
|
||||
[Fact]
|
||||
public void Rejects_symbol_path_as_a_non_string()
|
||||
=> TwinCATEquipmentTagParser.TryParse("""{"symbolPath":42}""", out _).ShouldBeFalse();
|
||||
|
||||
[Fact]
|
||||
public void Ignores_a_non_string_data_type_and_falls_back_to_default()
|
||||
{
|
||||
// dataType as a number is not a String enum — the guard ignores it and keeps the default.
|
||||
TwinCATEquipmentTagParser.TryParse("""{"symbolPath":"MAIN.X","dataType":3}""", out var def).ShouldBeTrue();
|
||||
def!.DataType.ShouldBe(TwinCATDataType.DInt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end driver-level proof: a TwinCAT driver with NO authored tags can still read an
|
||||
/// equipment-tag ref (the raw TagConfig JSON) — the resolver parses it into a transient
|
||||
/// definition (with a deviceHostAddress matching a configured device) and the read reaches
|
||||
/// the fake client instead of returning BadNodeIdUnknown.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Driver_resolves_an_equipment_ref_and_reads_instead_of_BadNodeIdUnknown()
|
||||
{
|
||||
var host = "ads://5.23.91.23.1.1:851";
|
||||
var json = $$"""{"deviceHostAddress":"{{host}}","symbolPath":"MAIN.Speed","dataType":"DInt"}""";
|
||||
var factory = new FakeTwinCATClientFactory { Customise = () => new FakeTwinCATClient { Values = { ["MAIN.Speed"] = 4242 } } };
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(host)],
|
||||
Tags = [], // no authored tags — resolution must come from the equipment-ref parser
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
}, "twincat-eq", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
var r = await drv.ReadAsync([json], CancellationToken.None);
|
||||
|
||||
r[0].StatusCode.ShouldBe(TwinCATStatusMapper.Good);
|
||||
r[0].StatusCode.ShouldNotBe(TwinCATStatusMapper.BadNodeIdUnknown);
|
||||
r[0].Value.ShouldBe(4242);
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -90,7 +90,7 @@ public sealed class TwinCATHighFindingsRegressionTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(DeviceA)],
|
||||
Tags = [new TwinCATTagDefinition("Big", DeviceA, "GVL.Big", TwinCATDataType.LInt)],
|
||||
RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("Big", DeviceA, "GVL.Big", TwinCATDataType.LInt)]),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
}, "drv-1", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
@@ -114,7 +114,7 @@ public sealed class TwinCATHighFindingsRegressionTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(DeviceA)],
|
||||
Tags = [new TwinCATTagDefinition("X", DeviceA, "GVL.X", TwinCATDataType.DInt)],
|
||||
RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("X", DeviceA, "GVL.X", TwinCATDataType.DInt)]),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
}, "drv-1", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
@@ -137,7 +137,7 @@ public sealed class TwinCATHighFindingsRegressionTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(DeviceA)],
|
||||
Tags = [new TwinCATTagDefinition("X", DeviceA, "GVL.X", TwinCATDataType.DInt)],
|
||||
RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("X", DeviceA, "GVL.X", TwinCATDataType.DInt)]),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
}, "drv-1", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
@@ -173,7 +173,7 @@ public sealed class TwinCATHighFindingsRegressionTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(DeviceA)],
|
||||
Tags = [new TwinCATTagDefinition("X", DeviceA, "GVL.X", TwinCATDataType.DInt)],
|
||||
RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("X", DeviceA, "GVL.X", TwinCATDataType.DInt)]),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
}, "drv-1", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
+8
-23
@@ -182,30 +182,15 @@ public sealed class TwinCATLowFindingsRegressionTests
|
||||
|
||||
// ---- Driver.TwinCAT-016 — gap-fill tests for previously closed findings ----
|
||||
|
||||
/// <summary>Verifies that structure-typed pre-declared tags are rejected at configuration parse time.</summary>
|
||||
/// <summary>Verifies that a Structure-typed raw tag is rejected by the tag-definition factory.</summary>
|
||||
[Fact]
|
||||
public void Structure_typed_pre_declared_tag_is_rejected_at_config_parse()
|
||||
public void Structure_typed_raw_tag_is_rejected_by_the_factory()
|
||||
{
|
||||
// Driver.TwinCAT-003 — config-time rejection. A Structure tag must fail loudly with a
|
||||
// clear error rather than reading as a garbage int blob or failing late on a write.
|
||||
var json = JsonSerializer.Serialize(new
|
||||
{
|
||||
devices = new[] { new { hostAddress = DeviceA } },
|
||||
tags = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
name = "Udt1",
|
||||
deviceHostAddress = DeviceA,
|
||||
symbolPath = "MAIN.fbInstance",
|
||||
dataType = "Structure",
|
||||
},
|
||||
},
|
||||
});
|
||||
var ex = Should.Throw<InvalidOperationException>(() =>
|
||||
TwinCATDriverFactoryExtensions.ParseOptionsForTests(json, "drv-1"));
|
||||
ex.Message.ShouldContain("Structure");
|
||||
ex.Message.ShouldContain("Udt1");
|
||||
// Driver.TwinCAT-003 — v3 the mapper is the sole tag path. A Structure tag must NOT map to a
|
||||
// definition (returns false ⇒ the driver skips it ⇒ BadNodeIdUnknown) rather than reading as a
|
||||
// garbage int blob or failing late on a write.
|
||||
var blob = """{"symbolPath":"MAIN.fbInstance","dataType":"Structure"}""";
|
||||
TwinCATTagDefinitionFactory.FromTagConfig(blob, "equip/Udt1", out _).ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the probe loop and read operations share one client per device.</summary>
|
||||
@@ -223,7 +208,7 @@ public sealed class TwinCATLowFindingsRegressionTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(DeviceA)],
|
||||
Tags = [new TwinCATTagDefinition("X", DeviceA, "GVL.X", TwinCATDataType.DInt)],
|
||||
RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("X", DeviceA, "GVL.X", TwinCATDataType.DInt)]),
|
||||
Probe = new TwinCATProbeOptions
|
||||
{
|
||||
Enabled = true,
|
||||
|
||||
+5
-5
@@ -15,7 +15,7 @@ public sealed class TwinCATNativeNotificationTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
|
||||
Tags = tags,
|
||||
RawTags = TwinCATRawTags.Entries(tags),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
UseNativeNotifications = true,
|
||||
}, "drv-1", factory);
|
||||
@@ -110,11 +110,11 @@ public sealed class TwinCATNativeNotificationTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
|
||||
Tags =
|
||||
RawTags = TwinCATRawTags.Entries(
|
||||
[
|
||||
new TwinCATTagDefinition("A", "ads://5.23.91.23.1.1:851", "MAIN.A", TwinCATDataType.DInt),
|
||||
new TwinCATTagDefinition("B", "ads://5.23.91.23.1.1:851", "MAIN.B", TwinCATDataType.DInt),
|
||||
],
|
||||
]),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
UseNativeNotifications = true,
|
||||
}, "drv-1", factory);
|
||||
@@ -182,7 +182,7 @@ public sealed class TwinCATNativeNotificationTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
|
||||
Tags = [new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt)],
|
||||
RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt)]),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
UseNativeNotifications = false,
|
||||
}, "drv-1", factory);
|
||||
@@ -216,7 +216,7 @@ public sealed class TwinCATNativeNotificationTests
|
||||
var drvPoll = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
|
||||
Tags = [new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt)],
|
||||
RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt)]),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
UseNativeNotifications = false,
|
||||
}, "drv-1", factoryPoll);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Test helper bridging the v2 authoring shape (a typed <see cref="TwinCATTagDefinition"/>) to the v3
|
||||
/// delivery shape (<see cref="RawTagEntry"/>). Under v3 the driver no longer takes pre-declared
|
||||
/// definitions — the deploy artifact hands it authored raw tags (RawPath + TagConfig blob + owning
|
||||
/// DeviceName), and the driver rebuilds the typed definitions via <see cref="TwinCATTagDefinitionFactory"/>.
|
||||
/// These tests keep expressing their intent as typed definitions; this helper serialises each back to its
|
||||
/// TagConfig blob (via <see cref="TwinCATTagDefinitionFactory.ToTagConfig"/>) and packages it as a
|
||||
/// <see cref="RawTagEntry"/> whose RawPath is the def's <c>Name</c> and whose
|
||||
/// <see cref="RawTagEntry.DeviceName"/> is the def's <c>DeviceHostAddress</c> — so the round-trip through
|
||||
/// the real mapper + the driver's device resolution (which matches DeviceName against the configured
|
||||
/// devices by host) reproduces the same definition + device routing the test authored, keyed by the same
|
||||
/// name the read/write/subscribe call sites use.
|
||||
/// </summary>
|
||||
internal static class TwinCATRawTags
|
||||
{
|
||||
/// <summary>Serialises one typed definition into its <see cref="RawTagEntry"/>
|
||||
/// (RawPath = def.Name, DeviceName = def.DeviceHostAddress).</summary>
|
||||
/// <param name="def">The typed definition to convert.</param>
|
||||
/// <returns>The equivalent <see cref="RawTagEntry"/>.</returns>
|
||||
public static RawTagEntry Entry(TwinCATTagDefinition def)
|
||||
=> new(def.Name, TwinCATTagDefinitionFactory.ToTagConfig(def), def.WriteIdempotent, def.DeviceHostAddress);
|
||||
|
||||
/// <summary>Serialises a sequence of typed definitions into <see cref="RawTagEntry"/> records.</summary>
|
||||
/// <param name="defs">The typed definitions to convert.</param>
|
||||
/// <returns>The equivalent raw-tag entries.</returns>
|
||||
public static IReadOnlyList<RawTagEntry> Entries(IEnumerable<TwinCATTagDefinition> defs)
|
||||
=> [.. defs.Select(Entry)];
|
||||
}
|
||||
@@ -14,7 +14,7 @@ public sealed class TwinCATReadWriteTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
|
||||
Tags = tags,
|
||||
RawTags = TwinCATRawTags.Entries(tags),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
}, "drv-1", factory);
|
||||
return (drv, factory);
|
||||
@@ -213,11 +213,11 @@ public sealed class TwinCATReadWriteTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
|
||||
Tags =
|
||||
RawTags = TwinCATRawTags.Entries(
|
||||
[
|
||||
new TwinCATTagDefinition("A", "ads://5.23.91.23.1.1:851", "MAIN.A", TwinCATDataType.DInt),
|
||||
new TwinCATTagDefinition("B", "ads://5.23.91.23.1.1:851", "MAIN.B", TwinCATDataType.DInt, Writable: false),
|
||||
],
|
||||
]),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
}, "drv-1", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
@@ -24,7 +24,7 @@ public sealed class TwinCATReconnectReplayTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(Host)],
|
||||
Tags = tags,
|
||||
RawTags = TwinCATRawTags.Entries(tags),
|
||||
Probe = new TwinCATProbeOptions
|
||||
{
|
||||
Enabled = probe,
|
||||
@@ -151,7 +151,7 @@ public sealed class TwinCATReconnectReplayTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(Host)],
|
||||
Tags = [new TwinCATTagDefinition("X", Host, "MAIN.X", TwinCATDataType.DInt)],
|
||||
RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("X", Host, "MAIN.X", TwinCATDataType.DInt)]),
|
||||
Probe = new TwinCATProbeOptions
|
||||
{
|
||||
Enabled = true,
|
||||
@@ -194,7 +194,7 @@ public sealed class TwinCATReconnectReplayTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(Host)],
|
||||
Tags = [new TwinCATTagDefinition("Speed", Host, "MAIN.Speed", TwinCATDataType.DInt)],
|
||||
RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("Speed", Host, "MAIN.Speed", TwinCATDataType.DInt)]),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
UseNativeNotifications = true,
|
||||
}, "drv-1", factory);
|
||||
@@ -240,7 +240,7 @@ public sealed class TwinCATReconnectReplayTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(Host)],
|
||||
Tags = [new TwinCATTagDefinition("Speed", Host, "MAIN.Speed", TwinCATDataType.DInt)],
|
||||
RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("Speed", Host, "MAIN.Speed", TwinCATDataType.DInt)]),
|
||||
Probe = new TwinCATProbeOptions
|
||||
{
|
||||
Enabled = true,
|
||||
@@ -302,7 +302,7 @@ public sealed class TwinCATReconnectReplayTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(Host)],
|
||||
Tags = [new TwinCATTagDefinition("X", Host, "MAIN.X", TwinCATDataType.DInt)],
|
||||
RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("X", Host, "MAIN.X", TwinCATDataType.DInt)]),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
UseNativeNotifications = true,
|
||||
}, "drv-1", factory);
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// CONV-4 — <see cref="TwinCATDriver.ResolveHost"/> must resolve an equipment-tag reference
|
||||
/// (raw TagConfig JSON) to its OWN device host so per-host breaker keys are correct for
|
||||
/// equipment tags, not the first-device fallback.
|
||||
/// CONV-4 — <see cref="TwinCATDriver.ResolveHost"/> must resolve an authored raw tag to its OWN
|
||||
/// device host so per-host breaker keys are correct for equipment tags, not the first-device
|
||||
/// fallback. Under v3 the tag is delivered as a <see cref="RawTagEntry"/> whose
|
||||
/// <see cref="RawTagEntry.DeviceName"/> names the owning device; the driver threads that onto the
|
||||
/// def's host at Initialize, and ResolveHost reads it back by RawPath.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class TwinCATResolveHostTests
|
||||
@@ -15,29 +18,35 @@ public sealed class TwinCATResolveHostTests
|
||||
private const string DeviceA = "ads://5.23.91.23.1.1:851";
|
||||
private const string DeviceB = "ads://5.23.91.23.1.2:851";
|
||||
|
||||
private static TwinCATDriver NewDriver() => new(
|
||||
new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(DeviceA), new TwinCATDeviceOptions(DeviceB)],
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
EnableControllerBrowse = false,
|
||||
},
|
||||
"twincat-1", new FakeTwinCATClientFactory());
|
||||
|
||||
/// <summary>An equipment-tag ref naming device B resolves to device B's host, not the first device.</summary>
|
||||
[Fact]
|
||||
public void ResolveHost_EquipmentTagRef_ReturnsItsOwnDeviceHost()
|
||||
private static async Task<TwinCATDriver> NewDriverAsync(params RawTagEntry[] rawTags)
|
||||
{
|
||||
var drv = NewDriver();
|
||||
var json = $$"""{"deviceHostAddress":"{{DeviceB}}","symbolPath":"MAIN.Speed","dataType":"DInt"}""";
|
||||
drv.ResolveHost(json).ShouldBe(DeviceB);
|
||||
var drv = new TwinCATDriver(
|
||||
new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(DeviceA), new TwinCATDeviceOptions(DeviceB)],
|
||||
RawTags = rawTags,
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
EnableControllerBrowse = false,
|
||||
},
|
||||
"twincat-1", new FakeTwinCATClientFactory());
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
return drv;
|
||||
}
|
||||
|
||||
/// <summary>A raw tag naming device B resolves to device B's host, not the first device.</summary>
|
||||
[Fact]
|
||||
public async Task ResolveHost_RawTagOnDeviceB_ReturnsItsOwnDeviceHost()
|
||||
{
|
||||
var blob = """{"symbolPath":"MAIN.Speed","dataType":"DInt"}""";
|
||||
var drv = await NewDriverAsync(new RawTagEntry("equip/Speed", blob, WriteIdempotent: false, DeviceName: DeviceB));
|
||||
drv.ResolveHost("equip/Speed").ShouldBe(DeviceB);
|
||||
}
|
||||
|
||||
/// <summary>An unresolvable ref keeps the current first-device fallback.</summary>
|
||||
[Fact]
|
||||
public void ResolveHost_UnknownRef_KeepsCurrentFallback()
|
||||
public async Task ResolveHost_UnknownRef_KeepsCurrentFallback()
|
||||
{
|
||||
var drv = NewDriver();
|
||||
var drv = await NewDriverAsync();
|
||||
drv.ResolveHost("totally-unknown").ShouldBe(DeviceA);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public sealed class TwinCATSymbolBrowserTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
|
||||
Tags = [new TwinCATTagDefinition("Declared", "ads://5.23.91.23.1.1:851", "MAIN.Declared", TwinCATDataType.DInt)],
|
||||
RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("Declared", "ads://5.23.91.23.1.1:851", "MAIN.Declared", TwinCATDataType.DInt)]),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
EnableControllerBrowse = false,
|
||||
}, "drv-1", factory);
|
||||
@@ -165,7 +165,7 @@ public sealed class TwinCATSymbolBrowserTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
|
||||
Tags = [new TwinCATTagDefinition("Declared", "ads://5.23.91.23.1.1:851", "MAIN.Declared", TwinCATDataType.DInt)],
|
||||
RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("Declared", "ads://5.23.91.23.1.1:851", "MAIN.Declared", TwinCATDataType.DInt)]),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
EnableControllerBrowse = true,
|
||||
}, "drv-1", factory);
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
|
||||
|
||||
/// <summary>R2-11 Phase C strictness surface for the TwinCAT tag-definition factory: the runtime is now STRICT —
|
||||
/// a typo'd <c>dataType</c> rejects the tag (<c>FromTagConfig</c> ⇒ <see langword="false"/> ⇒ <c>BadNodeIdUnknown</c>),
|
||||
/// <c>Inspect</c> reports the typo, and the <c>writable</c> key is honoured (default true).</summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class TwinCATTagDefinitionFactoryStrictnessTests
|
||||
{
|
||||
private const string RawPath = "area/line/equip/Tag";
|
||||
|
||||
[Fact]
|
||||
public void Typo_dataType_rejects_the_tag()
|
||||
{
|
||||
TwinCATTagDefinitionFactory.FromTagConfig("{\"symbolPath\":\"MAIN.x\",\"dataType\":\"DIntt\"}", RawPath, out _)
|
||||
.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Valid_dataType_still_parses()
|
||||
{
|
||||
TwinCATTagDefinitionFactory.FromTagConfig("{\"symbolPath\":\"MAIN.x\",\"dataType\":\"DInt\"}", RawPath, out var def)
|
||||
.ShouldBeTrue();
|
||||
def.DataType.ShouldBe(TwinCATDataType.DInt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Inspect_reports_typo_dataType()
|
||||
{
|
||||
var warnings = TwinCATTagDefinitionFactory.Inspect("{\"symbolPath\":\"MAIN.x\",\"dataType\":\"DIntt\"}");
|
||||
warnings.ShouldHaveSingleItem();
|
||||
warnings[0].ShouldContain("DIntt");
|
||||
warnings[0].ShouldContain("dataType");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Inspect_clean_config_has_no_warnings()
|
||||
{
|
||||
TwinCATTagDefinitionFactory.Inspect("{\"symbolPath\":\"MAIN.x\",\"dataType\":\"DInt\"}").ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Writable_false_is_honoured()
|
||||
{
|
||||
TwinCATTagDefinitionFactory.FromTagConfig("{\"symbolPath\":\"MAIN.x\",\"dataType\":\"DInt\",\"writable\":false}", RawPath, out var def)
|
||||
.ShouldBeTrue();
|
||||
def.Writable.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Writable_absent_defaults_to_true()
|
||||
{
|
||||
TwinCATTagDefinitionFactory.FromTagConfig("{\"symbolPath\":\"MAIN.x\",\"dataType\":\"DInt\"}", RawPath, out var def)
|
||||
.ShouldBeTrue();
|
||||
def.Writable.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public class TwinCATTagDefinitionFactoryTests
|
||||
{
|
||||
private const string RawPath = "area/line/equip/Speed";
|
||||
|
||||
[Fact]
|
||||
public void Maps_tagconfig_into_a_definition_keyed_by_rawpath()
|
||||
{
|
||||
// v3: deviceHostAddress is NOT read from the blob (dropped); routing comes from the RawTagEntry.
|
||||
var json = """{"symbolPath":"MAIN.Speed","dataType":"DInt"}""";
|
||||
TwinCATTagDefinitionFactory.FromTagConfig(json, RawPath, out var def).ShouldBeTrue();
|
||||
def!.Name.ShouldBe(RawPath); // identity is the RawPath, not the blob
|
||||
def.SymbolPath.ShouldBe("MAIN.Speed");
|
||||
def.DeviceHostAddress.ShouldBe(""); // mapper never reads a device from the blob
|
||||
def.DataType.ShouldBe(TwinCATDataType.DInt);
|
||||
def.Writable.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Defaults_optional_fields_when_only_symbol_path_is_present()
|
||||
{
|
||||
TwinCATTagDefinitionFactory.FromTagConfig("""{"symbolPath":"GVL.X"}""", RawPath, out var def).ShouldBeTrue();
|
||||
def!.SymbolPath.ShouldBe("GVL.X");
|
||||
def.DeviceHostAddress.ShouldBe("");
|
||||
def.DataType.ShouldBe(TwinCATDataType.DInt); // enum default
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rejects_a_non_address_blob()
|
||||
=> TwinCATTagDefinitionFactory.FromTagConfig("""{"FullName":"x"}""", RawPath, out _).ShouldBeFalse();
|
||||
|
||||
[Fact]
|
||||
public void Rejects_garbage()
|
||||
=> TwinCATTagDefinitionFactory.FromTagConfig("not json", RawPath, out _).ShouldBeFalse();
|
||||
|
||||
[Fact]
|
||||
public void Rejects_blank_tagconfig()
|
||||
=> TwinCATTagDefinitionFactory.FromTagConfig(" ", RawPath, out _).ShouldBeFalse();
|
||||
|
||||
[Fact]
|
||||
public void Rejects_symbol_path_as_a_non_string()
|
||||
=> TwinCATTagDefinitionFactory.FromTagConfig("""{"symbolPath":42}""", RawPath, out _).ShouldBeFalse();
|
||||
|
||||
[Fact]
|
||||
public void Ignores_a_non_string_data_type_and_falls_back_to_default()
|
||||
{
|
||||
// dataType as a number is not a String enum — the guard ignores it and keeps the default.
|
||||
TwinCATTagDefinitionFactory.FromTagConfig("""{"symbolPath":"MAIN.X","dataType":3}""", RawPath, out var def).ShouldBeTrue();
|
||||
def!.DataType.ShouldBe(TwinCATDataType.DInt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToTagConfig_round_trips_through_FromTagConfig()
|
||||
{
|
||||
var original = new TwinCATTagDefinition(
|
||||
Name: RawPath, DeviceHostAddress: "ads://5.23.91.23.1.1:851", SymbolPath: "MAIN.Speeds",
|
||||
DataType: TwinCATDataType.Real, Writable: false, ArrayLength: 4);
|
||||
var blob = TwinCATTagDefinitionFactory.ToTagConfig(original);
|
||||
|
||||
TwinCATTagDefinitionFactory.FromTagConfig(blob, RawPath, out var def).ShouldBeTrue();
|
||||
def!.SymbolPath.ShouldBe("MAIN.Speeds");
|
||||
def.DataType.ShouldBe(TwinCATDataType.Real);
|
||||
def.Writable.ShouldBeFalse();
|
||||
def.ArrayLength.ShouldBe(4);
|
||||
// Device + WriteIdempotent travel on the RawTagEntry, not the blob → mapper leaves them defaulted.
|
||||
def.DeviceHostAddress.ShouldBe("");
|
||||
def.WriteIdempotent.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end driver-level proof (v3 delivery): a TwinCAT driver served an authored raw tag
|
||||
/// (RawPath + TagConfig blob + owning DeviceName) resolves it by RawPath and routes it to the
|
||||
/// matching device — the read reaches the fake client instead of returning BadNodeIdUnknown.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Driver_resolves_a_raw_tag_and_reads_instead_of_BadNodeIdUnknown()
|
||||
{
|
||||
var host = "ads://5.23.91.23.1.1:851";
|
||||
var blob = """{"symbolPath":"MAIN.Speed","dataType":"DInt"}""";
|
||||
var factory = new FakeTwinCATClientFactory { Customise = () => new FakeTwinCATClient { Values = { ["MAIN.Speed"] = 4242 } } };
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(host)],
|
||||
RawTags = [new RawTagEntry(RawPath, blob, WriteIdempotent: false, DeviceName: host)],
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
}, "twincat-eq", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
var r = await drv.ReadAsync([RawPath], CancellationToken.None);
|
||||
|
||||
r[0].StatusCode.ShouldBe(TwinCATStatusMapper.Good);
|
||||
r[0].StatusCode.ShouldNotBe(TwinCATStatusMapper.BadNodeIdUnknown);
|
||||
r[0].Value.ShouldBe(4242);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user