Merge B1-ablegacy: v3 RawPath read-path

This commit is contained in:
Joseph Doherty
2026-07-15 20:20:49 -04:00
22 changed files with 458 additions and 287 deletions
@@ -94,9 +94,13 @@ public sealed class BuildOptionsTests
options.Devices[0].DeviceName.ShouldBe("cli-MicroLogix");
}
/// <summary>Verifies that tag list is forwarded verbatim to the options.</summary>
/// <summary>
/// Verifies that the typed tag list is delivered as v3 <see cref="RawTagEntry"/> records: RawPath =
/// the def's Name, DeviceName = the def's DeviceHostAddress, and the TagConfig blob round-trips the
/// def's address / dataType / writable through the real mapper.
/// </summary>
[Fact]
public void BuildOptions_forwards_tag_list_verbatim()
public void BuildOptions_delivers_tags_as_rawtag_entries()
{
var cmd = new TestCommand
{
@@ -107,7 +111,18 @@ public sealed class BuildOptionsTests
var options = cmd.Build(SampleTags);
options.Tags.ShouldBe(SampleTags);
options.RawTags.Count.ShouldBe(SampleTags.Count);
for (var i = 0; i < SampleTags.Count; i++)
{
var def = SampleTags[i];
var entry = options.RawTags[i];
entry.RawPath.ShouldBe(def.Name);
entry.DeviceName.ShouldBe(def.DeviceHostAddress);
AbLegacyTagDefinitionFactory.FromTagConfig(entry.TagConfig, entry.RawPath, out var round).ShouldBeTrue();
round.Address.ShouldBe(def.Address);
round.DataType.ShouldBe(def.DataType);
round.Writable.ShouldBe(def.Writable);
}
}
/// <summary>Verifies that timeout-ms option is propagated to the driver options.</summary>
@@ -139,7 +154,7 @@ public sealed class BuildOptionsTests
var options = cmd.Build([]);
options.Tags.ShouldBeEmpty();
options.RawTags.ShouldBeEmpty();
options.Devices.Count.ShouldBe(1);
options.Devices[0].PlcFamily.ShouldBe(AbLegacyPlcFamily.Plc5);
}
@@ -22,7 +22,7 @@ public sealed class AbLegacyArrayTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = tags,
RawTags = AbLegacyRawTags.Entries(tags),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "ablegacy-array", factory);
return (drv, factory);
@@ -169,7 +169,7 @@ public sealed class AbLegacyArrayTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition("Vector", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int, ArrayLength: 8)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Vector", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int, ArrayLength: 8)]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "ablegacy-array", new FakeAbLegacyTagFactory());
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -231,30 +231,30 @@ public sealed class AbLegacyArrayTests
// ---- Equipment-tag resolver 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 EquipmentTagParser_threads_arrayLength()
public void TagDefinitionFactory_threads_arrayLength()
{
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","isArray":true,"arrayLength":10}""";
AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
var json = """{"address":"N7:0","dataType":"Int","isArray":true,"arrayLength":10}""";
AbLegacyTagDefinitionFactory.FromTagConfig(json, "eq/a", out var def).ShouldBeTrue();
def!.ArrayLength.ShouldBe(10);
}
/// <summary>The parser caps <c>arrayLength</c> at the PCCC 256-word file maximum (isArray:true).</summary>
/// <summary>The factory caps <c>arrayLength</c> at the PCCC 256-word file maximum (isArray:true).</summary>
[Fact]
public void EquipmentTagParser_caps_arrayLength_at_256()
public void TagDefinitionFactory_caps_arrayLength_at_256()
{
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","isArray":true,"arrayLength":99999}""";
AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
var json = """{"address":"N7:0","dataType":"Int","isArray":true,"arrayLength":99999}""";
AbLegacyTagDefinitionFactory.FromTagConfig(json, "eq/a", out var def).ShouldBeTrue();
def!.ArrayLength.ShouldBe(256);
}
/// <summary>A scalar equipment tag (no arrayLength) leaves ArrayLength null.</summary>
/// <summary>A scalar tag (no arrayLength) leaves ArrayLength null.</summary>
[Fact]
public void EquipmentTagParser_scalar_leaves_arrayLength_null()
public void TagDefinitionFactory_scalar_leaves_arrayLength_null()
{
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int"}""";
AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
var json = """{"address":"N7:0","dataType":"Int"}""";
AbLegacyTagDefinitionFactory.FromTagConfig(json, "eq/a", out var def).ShouldBeTrue();
def!.ArrayLength.ShouldBeNull();
}
@@ -264,19 +264,19 @@ public sealed class AbLegacyArrayTests
/// cleared / absent isArray must leave ArrayLength null and never produce an orphan array.
/// </summary>
[Fact]
public void EquipmentTagParser_arrayLength_without_isArray_is_scalar()
public void TagDefinitionFactory_arrayLength_without_isArray_is_scalar()
{
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","arrayLength":8}""";
AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
var json = """{"address":"N7:0","dataType":"Int","arrayLength":8}""";
AbLegacyTagDefinitionFactory.FromTagConfig(json, "eq/a", out var def).ShouldBeTrue();
def!.ArrayLength.ShouldBeNull();
}
/// <summary>C-2 regression: <c>isArray:false</c> with a positive arrayLength is a SCALAR.</summary>
[Fact]
public void EquipmentTagParser_isArray_false_with_arrayLength_is_scalar()
public void TagDefinitionFactory_isArray_false_with_arrayLength_is_scalar()
{
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","isArray":false,"arrayLength":8}""";
AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
var json = """{"address":"N7:0","dataType":"Int","isArray":false,"arrayLength":8}""";
AbLegacyTagDefinitionFactory.FromTagConfig(json, "eq/a", out var def).ShouldBeTrue();
def!.ArrayLength.ShouldBeNull();
}
@@ -286,32 +286,33 @@ public sealed class AbLegacyArrayTests
/// as a 1-element array.
/// </summary>
[Fact]
public void EquipmentTagParser_isArray_true_arrayLength_one_is_one_element_array()
public void TagDefinitionFactory_isArray_true_arrayLength_one_is_one_element_array()
{
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","isArray":true,"arrayLength":1}""";
AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
var json = """{"address":"N7:0","dataType":"Int","isArray":true,"arrayLength":1}""";
AbLegacyTagDefinitionFactory.FromTagConfig(json, "eq/a", out var def).ShouldBeTrue();
def!.ArrayLength.ShouldBe(1);
}
/// <summary>
/// End-to-end: an AbLegacy driver with NO authored tags reads an equipment-tag ref whose
/// TagConfig carries <c>isArray:true</c> + <c>arrayLength</c> — the resolver threads the
/// count and the read surfaces a typed array, capping the libplctag element count at 256.
/// End-to-end: an AbLegacy driver reads a raw tag authored as a <see cref="RawTagEntry"/> whose
/// TagConfig carries <c>isArray:true</c> + <c>arrayLength</c> — the mapper threads the count and
/// the read surfaces a typed array, capping the libplctag element count at 256.
/// </summary>
[Fact]
public async Task Driver_reads_equipment_array_ref_as_typed_array()
public async Task Driver_reads_array_rawtag_as_typed_array()
{
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","isArray":true,"arrayLength":3}""";
const string rawPath = "cell/ablegacy/dev1/Vec";
var tagConfig = """{"address":"N7:0","dataType":"Int","isArray":true,"arrayLength":3}""";
var factory = new FakeAbLegacyTagFactory { Customise = p => new FakeAbLegacyTag(p) { ArrayValue = new short[] { 11, 22, 33 } } };
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [],
RawTags = [new RawTagEntry(rawPath, tagConfig, WriteIdempotent: false, DeviceName: "ab://10.0.0.5/1,0")],
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "ablegacy-eq-array", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
var r = await drv.ReadAsync([json], CancellationToken.None);
var r = await drv.ReadAsync([rawPath], CancellationToken.None);
r[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
var arr = r[0].Value.ShouldBeOfType<short[]>();
@@ -320,24 +321,25 @@ public sealed class AbLegacyArrayTests
}
/// <summary>
/// C-2 end-to-end: an equipment-tag ref carrying <c>isArray:false, arrayLength:8</c> reads
/// a single SCALAR value (the parser drops the orphan length), so the read never decodes a
/// C-2 end-to-end: a raw tag whose TagConfig carries <c>isArray:false, arrayLength:8</c> reads
/// a single SCALAR value (the mapper drops the orphan length), so the read never decodes a
/// phantom 8-element array against a scalar node. ElementCount stays 1.
/// </summary>
[Fact]
public async Task Driver_reads_isArray_false_with_arrayLength_as_scalar()
{
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","isArray":false,"arrayLength":8}""";
const string rawPath = "cell/ablegacy/dev1/Scl";
var tagConfig = """{"address":"N7:0","dataType":"Int","isArray":false,"arrayLength":8}""";
var factory = new FakeAbLegacyTagFactory { Customise = p => new FakeAbLegacyTag(p) { Value = 99 } };
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [],
RawTags = [new RawTagEntry(rawPath, tagConfig, WriteIdempotent: false, DeviceName: "ab://10.0.0.5/1,0")],
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "ablegacy-eq-scalar", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
var r = await drv.ReadAsync([json], CancellationToken.None);
var r = await drv.ReadAsync([rawPath], CancellationToken.None);
r[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
r[0].Value.ShouldBe(99);
@@ -80,7 +80,7 @@ public sealed class AbLegacyBitIndexRangeTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition("LBit20", "ab://10.0.0.5/1,0", "L9:0/20", AbLegacyDataType.Bit)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("LBit20", "ab://10.0.0.5/1,0", "L9:0/20", AbLegacyDataType.Bit)]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -105,7 +105,7 @@ public sealed class AbLegacyBitIndexRangeTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition("Bit0", "ab://10.0.0.5/1,0", "N7:0/0", AbLegacyDataType.Bit)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Bit0", "ab://10.0.0.5/1,0", "N7:0/0", AbLegacyDataType.Bit)]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -19,7 +19,7 @@ public sealed class AbLegacyBitRmwTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition("Flag3", "ab://10.0.0.5/1,0", "N7:0/3", AbLegacyDataType.Bit)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Flag3", "ab://10.0.0.5/1,0", "N7:0/3", AbLegacyDataType.Bit)]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -43,7 +43,7 @@ public sealed class AbLegacyBitRmwTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", "N7:0/3", AbLegacyDataType.Bit)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", "N7:0/3", AbLegacyDataType.Bit)]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -67,7 +67,7 @@ public sealed class AbLegacyBitRmwTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = tags,
RawTags = AbLegacyRawTags.Entries(tags),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -89,11 +89,11 @@ public sealed class AbLegacyBitRmwTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags =
RawTags = AbLegacyRawTags.Entries(
[
new AbLegacyTagDefinition("Bit0", "ab://10.0.0.5/1,0", "N7:0/0", AbLegacyDataType.Bit),
new AbLegacyTagDefinition("Bit5", "ab://10.0.0.5/1,0", "N7:0/5", AbLegacyDataType.Bit),
],
]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -120,7 +120,7 @@ public sealed class AbLegacyBitRmwTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", bitAddr, AbLegacyDataType.Bit)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", bitAddr, AbLegacyDataType.Bit)]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -143,7 +143,7 @@ public sealed class AbLegacyBitRmwTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", "B3:0/3", AbLegacyDataType.Bit)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", "B3:0/3", AbLegacyDataType.Bit)]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -167,7 +167,7 @@ public sealed class AbLegacyBitRmwTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = tags,
RawTags = AbLegacyRawTags.Entries(tags),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -195,7 +195,7 @@ public sealed class AbLegacyBitRmwTests
var opts = new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition("Bit2", "ab://10.0.0.5/1,0", "N7:0/2", AbLegacyDataType.Bit)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Bit2", "ab://10.0.0.5/1,0", "N7:0/2", AbLegacyDataType.Bit)]),
Probe = new AbLegacyProbeOptions { Enabled = false },
};
@@ -241,7 +241,7 @@ public sealed class AbLegacyBitRmwTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", "I:0/3", AbLegacyDataType.Bit)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", "I:0/3", AbLegacyDataType.Bit)]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -19,11 +19,11 @@ public sealed class AbLegacyCapabilityTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0", DeviceName: "Press-SLC-1")],
Tags =
RawTags = AbLegacyRawTags.Entries(
[
new AbLegacyTagDefinition("Speed", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int),
new AbLegacyTagDefinition("Temperature", "ab://10.0.0.5/1,0", "F8:0", AbLegacyDataType.Float, Writable: false),
],
]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1");
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -50,7 +50,7 @@ public sealed class AbLegacyCapabilityTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -75,7 +75,7 @@ public sealed class AbLegacyCapabilityTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -190,11 +190,11 @@ public sealed class AbLegacyCapabilityTests
new AbLegacyDeviceOptions("ab://10.0.0.5/1,0"),
new AbLegacyDeviceOptions("ab://10.0.0.6/1,0"),
],
Tags =
RawTags = AbLegacyRawTags.Entries(
[
new AbLegacyTagDefinition("A", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int),
new AbLegacyTagDefinition("B", "ab://10.0.0.6/1,0", "N7:0", AbLegacyDataType.Int),
],
]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1");
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -29,7 +29,7 @@ public sealed class AbLegacyDisposeAndResolveHostTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0", AbLegacyPlcFamily.Slc500)],
Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-dispose", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -72,7 +72,7 @@ public sealed class AbLegacyDisposeAndResolveHostTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -135,7 +135,7 @@ public sealed class AbLegacyDisposeAndResolveHostTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)]),
}, "drv-1");
drv.ResolveHost("X").ShouldBe("ab://10.0.0.5/1,0");
}
@@ -134,7 +134,7 @@ public sealed class AbLegacyDriverTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/", AbLegacyPlcFamily.Slc500)],
Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/", "N7:0", AbLegacyDataType.Int)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/", "N7:0", AbLegacyDataType.Int)]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -154,7 +154,7 @@ public sealed class AbLegacyDriverTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,2", AbLegacyPlcFamily.Slc500)],
Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,2", "N7:0", AbLegacyDataType.Int)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,2", "N7:0", AbLegacyDataType.Int)]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -171,7 +171,7 @@ public sealed class AbLegacyDriverTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/", AbLegacyPlcFamily.MicroLogix)],
Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/", "L9:0", AbLegacyDataType.Long)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/", "L9:0", AbLegacyDataType.Long)]),
}, "drv-1");
var ex = await Should.ThrowAsync<InvalidOperationException>(
@@ -188,7 +188,7 @@ public sealed class AbLegacyDriverTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0", AbLegacyPlcFamily.Slc500)],
Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "L9:0", AbLegacyDataType.Long)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "L9:0", AbLegacyDataType.Long)]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1");
@@ -209,7 +209,7 @@ public sealed class AbLegacyDriverTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0", AbLegacyPlcFamily.Plc5)],
Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "L9:0", AbLegacyDataType.Long)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "L9:0", AbLegacyDataType.Long)]),
}, "drv-1");
var ex = await Should.ThrowAsync<InvalidOperationException>(
@@ -4,30 +4,34 @@ using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
/// <summary>R2-11 Phase C strictness surface for the AbLegacy 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>
/// <summary>R2-11 Phase C strictness surface for the AbLegacy 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). Under v3 the mapped
/// definition's <c>Name</c> is the RawPath the factory was handed, not the blob.</summary>
public sealed class AbLegacyEquipmentTagParserStrictnessTests
{
private const string RawPath = "cell/ablegacy/dev1/T1";
[Fact]
public void Typo_dataType_rejects_the_tag()
{
AbLegacyEquipmentTagParser.TryParse("{\"address\":\"N7:0\",\"dataType\":\"Intt\"}", out _)
AbLegacyTagDefinitionFactory.FromTagConfig("{\"address\":\"N7:0\",\"dataType\":\"Intt\"}", RawPath, out _)
.ShouldBeFalse();
}
[Fact]
public void Valid_dataType_still_parses()
{
AbLegacyEquipmentTagParser.TryParse("{\"address\":\"N7:0\",\"dataType\":\"Int\"}", out var def)
AbLegacyTagDefinitionFactory.FromTagConfig("{\"address\":\"N7:0\",\"dataType\":\"Int\"}", RawPath, out var def)
.ShouldBeTrue();
def.DataType.ShouldBe(AbLegacyDataType.Int);
def.Name.ShouldBe(RawPath);
}
[Fact]
public void Inspect_reports_typo_dataType()
{
var warnings = AbLegacyEquipmentTagParser.Inspect("{\"address\":\"N7:0\",\"dataType\":\"Intt\"}");
var warnings = AbLegacyTagDefinitionFactory.Inspect("{\"address\":\"N7:0\",\"dataType\":\"Intt\"}");
warnings.ShouldHaveSingleItem();
warnings[0].ShouldContain("Intt");
warnings[0].ShouldContain("dataType");
@@ -36,13 +40,13 @@ public sealed class AbLegacyEquipmentTagParserStrictnessTests
[Fact]
public void Inspect_clean_config_has_no_warnings()
{
AbLegacyEquipmentTagParser.Inspect("{\"address\":\"N7:0\",\"dataType\":\"Int\"}").ShouldBeEmpty();
AbLegacyTagDefinitionFactory.Inspect("{\"address\":\"N7:0\",\"dataType\":\"Int\"}").ShouldBeEmpty();
}
[Fact]
public void Writable_false_is_honoured()
{
AbLegacyEquipmentTagParser.TryParse("{\"address\":\"N7:0\",\"dataType\":\"Int\",\"writable\":false}", out var def)
AbLegacyTagDefinitionFactory.FromTagConfig("{\"address\":\"N7:0\",\"dataType\":\"Int\",\"writable\":false}", RawPath, out var def)
.ShouldBeTrue();
def.Writable.ShouldBeFalse();
}
@@ -50,7 +54,7 @@ public sealed class AbLegacyEquipmentTagParserStrictnessTests
[Fact]
public void Writable_absent_defaults_to_true()
{
AbLegacyEquipmentTagParser.TryParse("{\"address\":\"N7:0\",\"dataType\":\"Int\"}", out var def)
AbLegacyTagDefinitionFactory.FromTagConfig("{\"address\":\"N7:0\",\"dataType\":\"Int\"}", RawPath, out var def)
.ShouldBeTrue();
def.Writable.ShouldBeTrue();
}
@@ -8,34 +8,38 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
[Trait("Category", "Unit")]
public sealed class AbLegacyEquipmentTagTests
{
private const string RawPath = "cell/ablegacy/dev1/Counter";
[Fact]
public void Parses_equipment_tagconfig_into_a_transient_definition()
public void Maps_tagconfig_into_a_definition_keyed_by_rawpath()
{
// v3: the deviceHostAddress key is dropped by the mapper (device routing travels on the
// RawTagEntry's DeviceName). The mapped definition is keyed by the RawPath, not the blob.
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int"}""";
AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
def!.Name.ShouldBe(json);
AbLegacyTagDefinitionFactory.FromTagConfig(json, RawPath, out var def).ShouldBeTrue();
def!.Name.ShouldBe(RawPath);
def.Address.ShouldBe("N7:0");
def.DataType.ShouldBe(AbLegacyDataType.Int);
def.DeviceHostAddress.ShouldBe("ab://10.0.0.5/1,0");
def.DeviceHostAddress.ShouldBe(""); // dropped — resolved from RawTagEntry.DeviceName at table-build time
}
[Fact]
public void Rejects_a_non_address_blob()
=> AbLegacyEquipmentTagParser.TryParse("""{"FullName":"x"}""", out _).ShouldBeFalse();
=> AbLegacyTagDefinitionFactory.FromTagConfig("""{"FullName":"x"}""", RawPath, out _).ShouldBeFalse();
[Fact]
public void Rejects_garbage()
=> AbLegacyEquipmentTagParser.TryParse("not json", out _).ShouldBeFalse();
=> AbLegacyTagDefinitionFactory.FromTagConfig("not json", RawPath, out _).ShouldBeFalse();
[Fact]
public void Rejects_address_as_a_json_number()
=> AbLegacyEquipmentTagParser.TryParse(
"""{"address":7,"dataType":"Int"}""", out _).ShouldBeFalse();
=> AbLegacyTagDefinitionFactory.FromTagConfig(
"""{"address":7,"dataType":"Int"}""", RawPath, out _).ShouldBeFalse();
[Fact]
public void Rejects_empty_address()
=> AbLegacyEquipmentTagParser.TryParse(
"""{"address":"","dataType":"Int"}""", out _).ShouldBeFalse();
=> AbLegacyTagDefinitionFactory.FromTagConfig(
"""{"address":"","dataType":"Int"}""", RawPath, out _).ShouldBeFalse();
// -002 regression: isArray:true without a valid positive arrayLength → scalar (null ArrayLength).
@@ -43,7 +47,7 @@ public sealed class AbLegacyEquipmentTagTests
public void IsArray_true_with_arrayLength_zero_produces_scalar()
{
var json = """{"address":"N7:0","dataType":"Int","isArray":true,"arrayLength":0}""";
AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
AbLegacyTagDefinitionFactory.FromTagConfig(json, RawPath, out var def).ShouldBeTrue();
def!.ArrayLength.ShouldBeNull();
}
@@ -51,7 +55,7 @@ public sealed class AbLegacyEquipmentTagTests
public void IsArray_true_with_no_arrayLength_produces_scalar()
{
var json = """{"address":"N7:0","dataType":"Int","isArray":true}""";
AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
AbLegacyTagDefinitionFactory.FromTagConfig(json, RawPath, out var def).ShouldBeTrue();
def!.ArrayLength.ShouldBeNull();
}
@@ -59,28 +63,30 @@ public sealed class AbLegacyEquipmentTagTests
public void IsArray_true_with_negative_arrayLength_produces_scalar()
{
var json = """{"address":"N7:0","dataType":"Int","isArray":true,"arrayLength":-5}""";
AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
AbLegacyTagDefinitionFactory.FromTagConfig(json, RawPath, out var def).ShouldBeTrue();
def!.ArrayLength.ShouldBeNull();
}
/// <summary>
/// End-to-end driver-level proof: an AbLegacy driver with NO authored tags can still read an
/// equipment-tag ref (the raw TagConfig JSON) — the resolver parses it into a transient
/// definition and the read goes to the fake runtime instead of returning BadNodeIdUnknown.
/// End-to-end driver-level proof: a raw tag authored as a <see cref="RawTagEntry"/> (RawPath
/// identity + TagConfig blob) is resolved by the driver's RawPath → def table and read against the
/// fake runtime, returning Good instead of BadNodeIdUnknown. Under v3 the read reference is always
/// a RawPath — the driver no longer parses a raw TagConfig blob handed as the reference.
/// </summary>
[Fact]
public async Task Driver_resolves_an_equipment_ref_and_reads_instead_of_BadNodeIdUnknown()
public async Task Driver_resolves_a_rawpath_ref_and_reads_instead_of_BadNodeIdUnknown()
{
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int"}""";
var tagConfig = """{"address":"N7:0","dataType":"Int"}""";
var factory = new FakeAbLegacyTagFactory { Customise = p => new FakeAbLegacyTag(p) { Value = 4242 } };
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [],
RawTags = [new RawTagEntry(RawPath, tagConfig, WriteIdempotent: false, DeviceName: "ab://10.0.0.5/1,0")],
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "ablegacy-eq", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
var r = await drv.ReadAsync([json], CancellationToken.None);
var r = await drv.ReadAsync([RawPath], CancellationToken.None);
r[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
r[0].StatusCode.ShouldNotBe(AbLegacyStatusMapper.BadNodeIdUnknown);
@@ -25,7 +25,7 @@ public sealed class AbLegacyEvictOnFailureTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions(Device)],
Tags = tags,
RawTags = AbLegacyRawTags.Entries(tags),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
return (drv, factory);
@@ -79,7 +79,7 @@ public sealed class AbLegacyLoggerInjectionTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-logged", factory, logger);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -0,0 +1,31 @@
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
/// <summary>
/// Test helper bridging the v2 authoring shape (a typed <see cref="AbLegacyTagDefinition"/>) 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="AbLegacyTagDefinitionFactory"/>. These tests keep expressing their intent as typed
/// definitions; this helper serialises each back to its TagConfig blob (via
/// <see cref="AbLegacyTagDefinitionFactory.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 reproduces the same definition
/// the test authored, keyed by the same name the read/write/subscribe call sites use and routed to the
/// same device (the driver matches DeviceName against a configured device's Name or HostAddress).
/// </summary>
internal static class AbLegacyRawTags
{
/// <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(AbLegacyTagDefinition def)
=> new(def.Name, AbLegacyTagDefinitionFactory.ToTagConfig(def), def.WriteIdempotent, DeviceName: def.DeviceHostAddress);
/// <summary>Serialises typed definitions into <see cref="RawTagEntry"/> records.</summary>
/// <param name="defs">The typed definitions to convert (array, params, or a collection expression).</param>
/// <returns>The equivalent raw-tag entries.</returns>
public static IReadOnlyList<RawTagEntry> Entries(params AbLegacyTagDefinition[] defs)
=> [.. defs.Select(Entry)];
}
@@ -15,7 +15,7 @@ public sealed class AbLegacyReadWriteTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = tags,
RawTags = AbLegacyRawTags.Entries(tags),
}, "drv-1", factory);
return (drv, factory);
}
@@ -179,7 +179,7 @@ public sealed class AbLegacyReadWriteTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition("Bit3", "ab://10.0.0.5/1,0", "N7:0/3", AbLegacyDataType.Bit)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Bit3", "ab://10.0.0.5/1,0", "N7:0/3", AbLegacyDataType.Bit)]),
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -206,7 +206,7 @@ public sealed class AbLegacyReadWriteTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition("Flag", "ab://10.0.0.5/1,0", "B3:0", AbLegacyDataType.Bit)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Flag", "ab://10.0.0.5/1,0", "B3:0", AbLegacyDataType.Bit)]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -245,11 +245,11 @@ public sealed class AbLegacyReadWriteTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags =
RawTags = AbLegacyRawTags.Entries(
[
new AbLegacyTagDefinition("A", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int),
new AbLegacyTagDefinition("B", "ab://10.0.0.5/1,0", "N7:1", AbLegacyDataType.Int, Writable: false),
],
]),
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -1,13 +1,15 @@
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
/// <summary>
/// CONV-4 — <see cref="AbLegacyDriver.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="AbLegacyDriver.ResolveHost"/> must resolve a RawPath reference to its OWN
/// device host so per-host breaker keys are correct for multi-device tags, not the first-device
/// fallback. v3: the tag's device travels on the <see cref="RawTagEntry.DeviceName"/> the deploy
/// artifact delivers; the driver resolves it to that device's host at table-build time.
/// </summary>
[Trait("Category", "Unit")]
public sealed class AbLegacyResolveHostTests
@@ -15,21 +17,25 @@ public sealed class AbLegacyResolveHostTests
private const string DeviceA = "ab://10.0.0.5/1,0";
private const string DeviceB = "ab://10.0.0.6/1,0";
private static AbLegacyDriver NewDriver() => new(
private static AbLegacyDriver NewDriver(params RawTagEntry[] rawTags) => new(
new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions(DeviceA), new AbLegacyDeviceOptions(DeviceB)],
RawTags = rawTags,
Probe = new AbLegacyProbeOptions { Enabled = false },
},
"ablegacy-1", new FakeAbLegacyTagFactory());
/// <summary>An equipment-tag ref naming device B resolves to device B's host, not the first device.</summary>
/// <summary>A raw tag owned by device B resolves to device B's host, not the first device.</summary>
[Fact]
public void ResolveHost_EquipmentTagRef_ReturnsItsOwnDeviceHost()
public async Task ResolveHost_RawTagRef_ReturnsItsOwnDeviceHost()
{
var drv = NewDriver();
var json = $$"""{"deviceHostAddress":"{{DeviceB}}","address":"N7:0","dataType":"Int"}""";
drv.ResolveHost(json).ShouldBe(DeviceB);
const string rawPath = "cell/ablegacy/devB/T1";
var drv = NewDriver(new RawTagEntry(
rawPath, """{"address":"N7:0","dataType":"Int"}""", WriteIdempotent: false, DeviceName: DeviceB));
await drv.InitializeAsync("{}", CancellationToken.None);
drv.ResolveHost(rawPath).ShouldBe(DeviceB);
}
/// <summary>An unresolvable ref keeps the current first-device fallback.</summary>
@@ -84,7 +84,7 @@ public sealed class AbLegacyRuntimeConcurrencyTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -117,7 +117,7 @@ public sealed class AbLegacyRuntimeConcurrencyTests
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)],
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)]),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);