test(v3): migrate 6 driver IntegrationTests to RawTags API

Replace pre-declared options.Tags (typed <Driver>TagDefinition lists) with
options.RawTags (IReadOnlyList<RawTagEntry>) across all 6 Docker-gated driver
integration suites so they COMPILE against the v3 driver API.

Each project gets a local <Driver>RawTags helper mirroring the unit-test
project's helper: it serialises a typed def back to its TagConfig blob via the
driver's <Driver>TagDefinitionFactory.ToTagConfig and packages it as a
RawTagEntry (RawPath = def.Name; DeviceName = def.DeviceHostAddress for the
multi-device drivers AbCip/AbLegacy/TwinCAT/FOCAS). Test intent (addresses,
data types, device routing, assertions) is unchanged — only the
tag-authoring/config-delivery shape was migrated.

Docker-gated: these still skip at runtime without fixtures; COMPILE is the gate.
This commit is contained in:
Joseph Doherty
2026-07-15 20:59:42 -04:00
parent 604928b29d
commit a4c61989b2
28 changed files with 249 additions and 104 deletions
@@ -0,0 +1,27 @@
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests;
/// <summary>
/// Test helper bridging the pre-v3 pre-declared-tag authoring style (a typed
/// <see cref="AbCipTagDefinition"/>) to the v3 RawPath seam the driver now consumes
/// (<see cref="RawTagEntry"/>). Each typed def is serialised to its <c>TagConfig</c> blob via
/// <see cref="AbCipTagDefinitionFactory.ToTagConfig"/>, and the def's identity (<c>Name</c> → RawPath),
/// device routing key (<c>DeviceHostAddress</c> → <see cref="RawTagEntry.DeviceName"/>) and
/// write-idempotence travel on the entry — exactly the shape the deploy artifact delivers. Lets the
/// driver's integration smoke tests keep expressing fixtures as typed definitions while exercising the
/// v3 <c>RawTags</c> → <c>FromTagConfig</c> table-build path. Mirrors the unit-test project's helper of
/// the same name.
/// </summary>
internal static class AbCipRawTags
{
/// <summary>Projects typed definitions to the driver's <c>RawTags</c> option shape.</summary>
/// <param name="defs">The typed definitions to project.</param>
/// <returns>The equivalent <see cref="RawTagEntry"/> list.</returns>
public static IReadOnlyList<RawTagEntry> From(params AbCipTagDefinition[] defs) =>
[.. defs.Select(d => new RawTagEntry(
RawPath: d.Name,
TagConfig: AbCipTagDefinitionFactory.ToTagConfig(d),
WriteIdempotent: d.WriteIdempotent,
DeviceName: d.DeviceHostAddress))];
}
@@ -37,7 +37,7 @@ public sealed class AbCipReadSmokeTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(deviceUri, profile.Family)],
Tags = [new AbCipTagDefinition("Counter", deviceUri, "TestDINT", AbCipDataType.DInt)],
RawTags = AbCipRawTags.From(new AbCipTagDefinition("Counter", deviceUri, "TestDINT", AbCipDataType.DInt)),
Timeout = TimeSpan.FromSeconds(5),
}, $"drv-smoke-{profile.Family}");
@@ -54,7 +54,7 @@ public sealed class AbCipEmulateAlmdTests
Devices = [new AbCipDeviceOptions($"ab://{endpoint}/1,0")],
EnableAlarmProjection = true,
AlarmPollInterval = TimeSpan.FromMilliseconds(200),
Tags = [
RawTags = AbCipRawTags.From(
new AbCipTagDefinition(
Name: "HighTempAlarm",
DeviceHostAddress: $"ab://{endpoint}/1,0",
@@ -72,8 +72,7 @@ public sealed class AbCipEmulateAlmdTests
DeviceHostAddress: $"ab://{endpoint}/1,0",
TagPath: "SimulateAlarm",
DataType: AbCipDataType.Bool,
Writable: true),
],
Writable: true)),
};
await using var drv = new AbCipDriver(options, driverInstanceId: "emulate-almd");
@@ -51,7 +51,7 @@ public sealed class AbCipEmulateUdtReadTests
var options = new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions($"ab://{endpoint}/1,0")],
Tags = [
RawTags = AbCipRawTags.From(
new AbCipTagDefinition(
Name: "Motor1",
DeviceHostAddress: $"ab://{endpoint}/1,0",
@@ -61,8 +61,7 @@ public sealed class AbCipEmulateUdtReadTests
new AbCipStructureMember("Speed", AbCipDataType.DInt),
new AbCipStructureMember("Torque", AbCipDataType.Real),
new AbCipStructureMember("Status", AbCipDataType.DInt),
]),
],
])),
Timeout = TimeSpan.FromSeconds(5),
};
@@ -0,0 +1,32 @@
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests;
/// <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 integration smoke 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).
/// Mirrors the unit-test project's helper of the same name.
/// </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)];
}
@@ -51,13 +51,12 @@ public sealed class AbLegacyReadSmokeTests(AbLegacyServerFixture sim)
await using var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions(deviceUri, profile.Family)],
Tags = [
RawTags = AbLegacyRawTags.Entries(
new AbLegacyTagDefinition(
Name: "IntCounter",
DeviceHostAddress: deviceUri,
Address: "N7:0",
DataType: AbLegacyDataType.Int),
],
DataType: AbLegacyDataType.Int)),
Timeout = TimeSpan.FromSeconds(5),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, driverInstanceId: $"ablegacy-smoke-{profile.Family}");
@@ -96,14 +95,13 @@ public sealed class AbLegacyReadSmokeTests(AbLegacyServerFixture sim)
await using var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions(deviceUri, AbLegacyPlcFamily.Slc500)],
Tags = [
RawTags = AbLegacyRawTags.Entries(
new AbLegacyTagDefinition(
Name: "Scratch",
DeviceHostAddress: deviceUri,
Address: "N7:5",
DataType: AbLegacyDataType.Int,
Writable: true),
],
Writable: true)),
Timeout = TimeSpan.FromSeconds(5),
Probe = new AbLegacyProbeOptions { Enabled = false },
}, driverInstanceId: "ablegacy-smoke-rw");
@@ -0,0 +1,31 @@
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests;
/// <summary>
/// Test helper bridging the v2 authoring shape (a typed <see cref="FocasTagDefinition"/>) 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 + DeviceName),
/// and the driver rebuilds the typed definitions via <see cref="FocasTagDefinitionFactory"/> and routes
/// each to its device by <see cref="RawTagEntry.DeviceName"/>. These integration wire tests keep
/// expressing their intent as typed definitions; this helper serialises each back to its TagConfig blob
/// (via <see cref="FocasTagDefinitionFactory.ToTagConfig"/>) and packages it as a <see cref="RawTagEntry"/>
/// whose RawPath is the def's <c>Name</c> and whose DeviceName is the def's <c>DeviceHostAddress</c> — so
/// the round-trip through the real mapper + device match reproduces the same definition the test authored,
/// keyed by the same name the read/write/subscribe call sites use. Mirrors the unit-test project's helper
/// of the same name.
/// </summary>
internal static class FocasRawTags
{
/// <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(FocasTagDefinition def)
=> new(def.Name, FocasTagDefinitionFactory.ToTagConfig(def), def.WriteIdempotent, 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 FocasTagDefinition[] defs)
=> [.. defs.Select(Entry)];
}
@@ -54,12 +54,10 @@ public sealed class WireBackendCoverageTests
var drv = new FocasDriver(new FocasDriverOptions
{
Devices = [new FocasDeviceOptions(DeviceHost)],
Tags =
[
RawTags = FocasRawTags.Entries(
new FocasTagDefinition("Param6711", DeviceHost, "PARAM:6711", FocasDataType.Int32, Writable: false),
new FocasTagDefinition("Macro500", DeviceHost, "MACRO:500", FocasDataType.Float64, Writable: false),
new FocasTagDefinition("R100", DeviceHost, "R100", FocasDataType.Byte, Writable: false),
],
new FocasTagDefinition("R100", DeviceHost, "R100", FocasDataType.Byte, Writable: false)),
Probe = new FocasProbeOptions { Enabled = false },
}, driverInstanceId: "wire-usertags", clientFactory: new WireFocasClientFactory());
@@ -86,11 +84,9 @@ public sealed class WireBackendCoverageTests
var drv = new FocasDriver(new FocasDriverOptions
{
Devices = [new FocasDeviceOptions(DeviceHost, DeviceName: "Lathe-1")],
Tags =
[
RawTags = FocasRawTags.Entries(
new FocasTagDefinition("Run", DeviceHost, "R100", FocasDataType.Byte, Writable: false),
new FocasTagDefinition("Speed", DeviceHost, "MACRO:500", FocasDataType.Float64, Writable: false),
],
new FocasTagDefinition("Speed", DeviceHost, "MACRO:500", FocasDataType.Float64, Writable: false)),
Probe = new FocasProbeOptions { Enabled = false },
}, driverInstanceId: "wire-discover", clientFactory: new WireFocasClientFactory());
@@ -127,7 +123,7 @@ public sealed class WireBackendCoverageTests
var drv = new FocasDriver(new FocasDriverOptions
{
Devices = [new FocasDeviceOptions(DeviceHost)],
Tags = [new FocasTagDefinition("Run", DeviceHost, "R100", FocasDataType.Byte, Writable: false)],
RawTags = FocasRawTags.Entries(new FocasTagDefinition("Run", DeviceHost, "R100", FocasDataType.Byte, Writable: false)),
Probe = new FocasProbeOptions { Enabled = false },
}, driverInstanceId: "wire-subscribe", clientFactory: new WireFocasClientFactory());
@@ -172,7 +168,7 @@ public sealed class WireBackendCoverageTests
var drv = new FocasDriver(new FocasDriverOptions
{
Devices = [new FocasDeviceOptions(DeviceHost)],
Tags = [],
RawTags = [],
Probe = new FocasProbeOptions { Enabled = false },
AlarmProjection = new FocasAlarmProjectionOptions
{
@@ -275,7 +271,7 @@ public sealed class WireBackendCoverageTests
var driver = new FocasDriver(new FocasDriverOptions
{
Devices = [new FocasDeviceOptions(DeviceHost, PositionDecimalPlaces: 1)],
Tags = [],
RawTags = [],
Probe = new FocasProbeOptions { Enabled = false },
FixedTree = new FocasFixedTreeOptions
{
@@ -57,7 +57,7 @@ public sealed class WireBackendTests
var driver = new FocasDriver(new FocasDriverOptions
{
Devices = [new FocasDeviceOptions(DeviceHost)],
Tags = [],
RawTags = [],
Probe = new FocasProbeOptions { Enabled = false },
FixedTree = new FocasFixedTreeOptions
{
@@ -120,7 +120,7 @@ public sealed class WireBackendTests
var driver = new FocasDriver(new FocasDriverOptions
{
Devices = [new FocasDeviceOptions(DeviceHost)],
Tags = [],
RawTags = [],
Probe = new FocasProbeOptions { Enabled = false },
FixedTree = new FocasFixedTreeOptions
{
@@ -175,7 +175,7 @@ public sealed class WireBackendTests
var driver = new FocasDriver(new FocasDriverOptions
{
Devices = [new FocasDeviceOptions(DeviceHost)],
Tags = [],
RawTags = [],
Probe = new FocasProbeOptions { Enabled = false },
FixedTree = new FocasFixedTreeOptions
{
@@ -242,7 +242,7 @@ public sealed class WireBackendTests
var driver = new FocasDriver(new FocasDriverOptions
{
Devices = [new FocasDeviceOptions(DeviceHost)],
Tags = [],
RawTags = [],
Probe = new FocasProbeOptions { Enabled = false },
FixedTree = new FocasFixedTreeOptions
{
@@ -27,7 +27,7 @@ public sealed class AddressingGrammarTests
{
var opts = new ModbusDriverOptions
{
Host = _sim.Host, Port = _sim.Port, Tags = tags,
Host = _sim.Host, Port = _sim.Port, RawTags = ModbusRawTags.Entries(tags),
Probe = new ModbusProbeOptions { Enabled = false },
};
var drv = new ModbusDriver(opts, "addressing-e2e");
@@ -108,7 +108,7 @@ public sealed class AddressingGrammarTests
var t3 = new ModbusTagDefinition("T3", ModbusRegion.HoldingRegisters, 204, ModbusDataType.Int16);
var opts = new ModbusDriverOptions
{
Host = _sim.Host, Port = _sim.Port, Tags = [t1, t2, t3], MaxReadGap = 5,
Host = _sim.Host, Port = _sim.Port, RawTags = ModbusRawTags.Entries([t1, t2, t3]), MaxReadGap = 5,
Probe = new ModbusProbeOptions { Enabled = false },
};
var drv = new ModbusDriver(opts, "addressing-e2e");
@@ -31,15 +31,14 @@ public sealed class DL205BcdQuirkTests(ModbusSimulatorFixture sim)
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
RawTags = ModbusRawTags.Entries([
new ModbusTagDefinition("DL205_Count_Bcd",
ModbusRegion.HoldingRegisters, Address: 1072,
DataType: ModbusDataType.Bcd16, Writable: false),
new ModbusTagDefinition("DL205_Count_Int16",
ModbusRegion.HoldingRegisters, Address: 1072,
DataType: ModbusDataType.Int16, Writable: false),
],
]),
Probe = new ModbusProbeOptions { Enabled = false },
};
await using var driver = new ModbusDriver(options, driverInstanceId: "dl205-bcd");
@@ -106,7 +106,7 @@ public sealed class DL205CoilMappingTests(ModbusSimulatorFixture sim)
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags = tags,
RawTags = ModbusRawTags.Entries(tags),
Probe = new ModbusProbeOptions { Enabled = false },
};
}
@@ -36,12 +36,11 @@ public sealed class DL205ExceptionCodeTests(ModbusSimulatorFixture sim)
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
RawTags = ModbusRawTags.Entries([
new ModbusTagDefinition("Unmapped",
ModbusRegion.HoldingRegisters, Address: 16383,
DataType: ModbusDataType.UInt16, Writable: false),
],
]),
Probe = new ModbusProbeOptions { Enabled = false },
};
await using var driver = new ModbusDriver(options, driverInstanceId: "dl205-exc");
@@ -33,8 +33,7 @@ public sealed class DL205FloatCdabQuirkTests(ModbusSimulatorFixture sim)
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
RawTags = ModbusRawTags.Entries([
new ModbusTagDefinition("DL205_Float_CDAB",
ModbusRegion.HoldingRegisters, Address: 1056,
DataType: ModbusDataType.Float32, Writable: false,
@@ -44,7 +43,7 @@ public sealed class DL205FloatCdabQuirkTests(ModbusSimulatorFixture sim)
ModbusRegion.HoldingRegisters, Address: 1056,
DataType: ModbusDataType.Float32, Writable: false,
ByteOrder: ModbusByteOrder.BigEndian),
],
]),
Probe = new ModbusProbeOptions { Enabled = false },
};
await using var driver = new ModbusDriver(options, driverInstanceId: "dl205-cdab");
@@ -37,15 +37,14 @@ public static class DL205Profile
Port = port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
RawTags = ModbusRawTags.Entries([
new ModbusTagDefinition(
Name: "Smoke_HReg200",
Region: ModbusRegion.HoldingRegisters,
Address: SmokeHoldingRegister,
DataType: ModbusDataType.Int16,
Writable: true),
],
]),
// Disable the background probe loop — integration tests drive reads explicitly and
// the probe would race with assertions.
Probe = new ModbusProbeOptions { Enabled = false },
@@ -40,8 +40,7 @@ public sealed class DL205StringQuirkTests(ModbusSimulatorFixture sim)
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
RawTags = ModbusRawTags.Entries([
new ModbusTagDefinition(
Name: "DL205_Hello_Low",
Region: ModbusRegion.HoldingRegisters,
@@ -60,7 +59,7 @@ public sealed class DL205StringQuirkTests(ModbusSimulatorFixture sim)
Writable: false,
StringLength: 5,
StringByteOrder: ModbusStringByteOrder.HighByteFirst),
],
]),
Probe = new ModbusProbeOptions { Enabled = false },
};
await using var driver = new ModbusDriver(options, driverInstanceId: "dl205-string");
@@ -36,12 +36,11 @@ public sealed class DL205VMemoryQuirkTests(ModbusSimulatorFixture sim)
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
RawTags = ModbusRawTags.Entries([
new ModbusTagDefinition("DL205_V2000",
ModbusRegion.HoldingRegisters, Address: pdu,
DataType: ModbusDataType.UInt16, Writable: false),
],
]),
Probe = new ModbusProbeOptions { Enabled = false },
};
await using var driver = new ModbusDriver(options, driverInstanceId: "dl205-vmem");
@@ -75,12 +74,11 @@ public sealed class DL205VMemoryQuirkTests(ModbusSimulatorFixture sim)
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
RawTags = ModbusRawTags.Entries([
new ModbusTagDefinition("DL205_V40400",
ModbusRegion.HoldingRegisters, Address: pdu,
DataType: ModbusDataType.UInt16, Writable: false),
],
]),
Probe = new ModbusProbeOptions { Enabled = false },
};
await using var driver = new ModbusDriver(options, driverInstanceId: "dl205-sysv");
@@ -66,7 +66,7 @@ public sealed class DL205XInputTests(ModbusSimulatorFixture sim)
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags = tags,
RawTags = ModbusRawTags.Entries(tags),
Probe = new ModbusProbeOptions { Enabled = false },
};
}
@@ -47,12 +47,11 @@ public sealed class ExceptionInjectionTests(ModbusSimulatorFixture sim)
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
RawTags = ModbusRawTags.Entries([
new ModbusTagDefinition(tagName,
ModbusRegion.HoldingRegisters, Address: (ushort)address,
DataType: ModbusDataType.UInt16, Writable: false),
],
]),
Probe = new ModbusProbeOptions { Enabled = false },
};
await using var driver = new ModbusDriver(opts, driverInstanceId: "modbus-exc");
@@ -112,12 +111,11 @@ public sealed class ExceptionInjectionTests(ModbusSimulatorFixture sim)
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
RawTags = ModbusRawTags.Entries([
new ModbusTagDefinition(tag,
ModbusRegion.HoldingRegisters, Address: (ushort)address,
DataType: ModbusDataType.UInt16, Writable: true),
],
]),
Probe = new ModbusProbeOptions { Enabled = false },
};
await using var driver = new ModbusDriver(opts, driverInstanceId: "modbus-exc-write");
@@ -32,15 +32,14 @@ public static class MitsubishiProfile
Port = port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
RawTags = ModbusRawTags.Entries([
new ModbusTagDefinition(
Name: "Smoke_HReg200",
Region: ModbusRegion.HoldingRegisters,
Address: SmokeHoldingRegister,
DataType: ModbusDataType.Int16,
Writable: true),
],
]),
Probe = new ModbusProbeOptions { Enabled = false },
};
}
@@ -176,7 +176,7 @@ public sealed class MitsubishiQuirkTests(ModbusSimulatorFixture sim)
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags = tags,
RawTags = ModbusRawTags.Entries(tags),
Probe = new ModbusProbeOptions { Enabled = false },
},
driverInstanceId: "melsec-quirk");
@@ -0,0 +1,29 @@
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests;
/// <summary>
/// Test helper bridging the v2 authoring shape (a typed <see cref="ModbusTagDefinition"/>) 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), and the
/// driver rebuilds the typed definitions via <see cref="ModbusTagDefinitionFactory"/>. These integration
/// profiles + quirk tests keep expressing their intent as typed definitions; this helper serialises each
/// back to its TagConfig blob (via <see cref="ModbusTagDefinitionFactory.ToTagConfig"/>) and packages it
/// as a <see cref="RawTagEntry"/> whose RawPath is the def's <c>Name</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. Mirrors the unit-test project's helper of the same name.
/// </summary>
internal static class ModbusRawTags
{
/// <summary>Serialises one typed definition into its <see cref="RawTagEntry"/> (RawPath = def.Name).</summary>
/// <param name="def">The typed definition to convert.</param>
/// <returns>The equivalent <see cref="RawTagEntry"/>.</returns>
public static RawTagEntry Entry(ModbusTagDefinition def)
=> new(def.Name, ModbusTagDefinitionFactory.ToTagConfig(def), def.WriteIdempotent);
/// <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<ModbusTagDefinition> defs)
=> [.. defs.Select(Entry)];
}
@@ -32,15 +32,14 @@ public static class S7_1500Profile
Port = port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
RawTags = ModbusRawTags.Entries([
new ModbusTagDefinition(
Name: "Smoke_HReg200",
Region: ModbusRegion.HoldingRegisters,
Address: SmokeHoldingRegister,
DataType: ModbusDataType.Int16,
Writable: true),
],
]),
// Disable the background probe loop — integration tests drive reads explicitly and
// the probe would race with assertions.
Probe = new ModbusProbeOptions { Enabled = false },
@@ -33,8 +33,7 @@ public sealed class S7_ByteOrderTests(ModbusSimulatorFixture sim)
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
RawTags = ModbusRawTags.Entries([
new ModbusTagDefinition("S7_Float_ABCD",
ModbusRegion.HoldingRegisters, Address: 100,
DataType: ModbusDataType.Float32, Writable: false,
@@ -45,7 +44,7 @@ public sealed class S7_ByteOrderTests(ModbusSimulatorFixture sim)
ModbusRegion.HoldingRegisters, Address: 100,
DataType: ModbusDataType.Float32, Writable: false,
ByteOrder: ModbusByteOrder.WordSwap),
],
]),
Probe = new ModbusProbeOptions { Enabled = false },
};
await using var driver = new ModbusDriver(options, driverInstanceId: "s7-float-abcd");
@@ -79,13 +78,12 @@ public sealed class S7_ByteOrderTests(ModbusSimulatorFixture sim)
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
RawTags = ModbusRawTags.Entries([
new ModbusTagDefinition("S7_Int32_ABCD",
ModbusRegion.HoldingRegisters, Address: 300,
DataType: ModbusDataType.Int32, Writable: false,
ByteOrder: ModbusByteOrder.BigEndian),
],
]),
Probe = new ModbusProbeOptions { Enabled = false },
};
await using var driver = new ModbusDriver(options, driverInstanceId: "s7-int-abcd");
@@ -117,12 +115,11 @@ public sealed class S7_ByteOrderTests(ModbusSimulatorFixture sim)
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
RawTags = ModbusRawTags.Entries([
new ModbusTagDefinition("S7_Fingerprint",
ModbusRegion.HoldingRegisters, Address: 0,
DataType: ModbusDataType.UInt16, Writable: false),
],
]),
Probe = new ModbusProbeOptions { Enabled = false },
};
await using var driver = new ModbusDriver(options, driverInstanceId: "s7-fingerprint");
@@ -0,0 +1,29 @@
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests;
/// <summary>
/// Test helper bridging the v2 authoring shape (a typed <see cref="S7TagDefinition"/>) 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), and the
/// driver rebuilds the typed definitions via <see cref="S7TagDefinitionFactory"/>. These integration
/// profiles keep expressing their intent as typed definitions; this helper serialises each back to its
/// TagConfig blob (via <see cref="S7TagDefinitionFactory.ToTagConfig"/>) and packages it as a
/// <see cref="RawTagEntry"/> whose RawPath is the def's <c>Name</c> — so the round-trip through the real
/// mapper reproduces the same definition the profile authored, keyed by the same name the
/// read/write/subscribe call sites use. Mirrors the unit-test project's helper of the same name.
/// </summary>
internal static class S7RawTags
{
/// <summary>Serialises one typed definition into its <see cref="RawTagEntry"/> (RawPath = def.Name).</summary>
/// <param name="def">The typed definition to convert.</param>
/// <returns>The equivalent <see cref="RawTagEntry"/>.</returns>
public static RawTagEntry Entry(S7TagDefinition def)
=> new(def.Name, S7TagDefinitionFactory.ToTagConfig(def), def.WriteIdempotent);
/// <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(params S7TagDefinition[] defs)
=> [.. defs.Select(Entry)];
}
@@ -41,14 +41,12 @@ public static class S7_1500Profile
// gate, injecting flakiness that has nothing to do with the code
// under test.
Probe = new S7ProbeOptions { Enabled = false },
Tags =
[
RawTags = S7RawTags.Entries(
new S7TagDefinition(ProbeTag, "DB1.DBW0", S7DataType.UInt16),
new S7TagDefinition(SmokeI16Tag, "DB1.DBW10", S7DataType.Int16),
new S7TagDefinition(SmokeI32Tag, "DB1.DBD20", S7DataType.Int32),
new S7TagDefinition(SmokeF32Tag, "DB1.DBD30", S7DataType.Float32),
new S7TagDefinition(SmokeBoolTag, "DB1.DBX50.3", S7DataType.Bool),
new S7TagDefinition(WriteScratchTag, "DB1.DBW100", S7DataType.UInt16),
],
new S7TagDefinition(WriteScratchTag, "DB1.DBW100", S7DataType.UInt16)),
};
}
@@ -246,14 +246,12 @@ public sealed class TwinCAT3SmokeTests(TwinCATXarFixture sim)
var options = new TwinCATDriverOptions
{
Devices = [new TwinCATDeviceOptions($"ads://{sim.TargetNetId}:{sim.AmsPort}", "XAR-VM")],
Tags =
[
RawTags = TwinCATRawTags.Entries(
new TwinCATTagDefinition(
Name: "Primitive",
DeviceHostAddress: $"ads://{sim.TargetNetId}:{sim.AmsPort}",
SymbolPath: symbolPath,
DataType: type),
],
DataType: type)),
Timeout = TimeSpan.FromSeconds(5),
Probe = new TwinCATProbeOptions { Enabled = false },
};
@@ -307,8 +305,7 @@ public sealed class TwinCAT3SmokeTests(TwinCATXarFixture sim)
var options = new TwinCATDriverOptions
{
Devices = [new TwinCATDeviceOptions($"ads://{sim.TargetNetId}:{sim.AmsPort}", "XAR-VM")],
Tags =
[
RawTags = TwinCATRawTags.Entries(
new TwinCATTagDefinition(
Name: "WordBit3",
DeviceHostAddress: $"ads://{sim.TargetNetId}:{sim.AmsPort}",
@@ -318,8 +315,7 @@ public sealed class TwinCAT3SmokeTests(TwinCATXarFixture sim)
Name: "WordBit4",
DeviceHostAddress: $"ads://{sim.TargetNetId}:{sim.AmsPort}",
SymbolPath: "GVL_Primitives.vWord.4",
DataType: TwinCATDataType.Bool),
],
DataType: TwinCATDataType.Bool)),
Timeout = TimeSpan.FromSeconds(5),
Probe = new TwinCATProbeOptions { Enabled = false },
};
@@ -345,8 +341,7 @@ public sealed class TwinCAT3SmokeTests(TwinCATXarFixture sim)
var options = new TwinCATDriverOptions
{
Devices = [new TwinCATDeviceOptions($"ads://{sim.TargetNetId}:{sim.AmsPort}", "XAR-VM")],
Tags =
[
RawTags = TwinCATRawTags.Entries(
new TwinCATTagDefinition(
Name: "MotorTemp",
DeviceHostAddress: $"ads://{sim.TargetNetId}:{sim.AmsPort}",
@@ -356,8 +351,7 @@ public sealed class TwinCAT3SmokeTests(TwinCATXarFixture sim)
Name: "MotorRunning",
DeviceHostAddress: $"ads://{sim.TargetNetId}:{sim.AmsPort}",
SymbolPath: "GVL_Plant.Line1.Stations[1].Axes[1].Motor.Running",
DataType: TwinCATDataType.Bool),
],
DataType: TwinCATDataType.Bool)),
Timeout = TimeSpan.FromSeconds(5),
Probe = new TwinCATProbeOptions { Enabled = false },
};
@@ -386,8 +380,7 @@ public sealed class TwinCAT3SmokeTests(TwinCATXarFixture sim)
var options = new TwinCATDriverOptions
{
Devices = [new TwinCATDeviceOptions($"ads://{sim.TargetNetId}:{sim.AmsPort}", "XAR-VM")],
Tags =
[
RawTags = TwinCATRawTags.Entries(
new TwinCATTagDefinition(
Name: "NonexistentSymbol",
DeviceHostAddress: $"ads://{sim.TargetNetId}:{sim.AmsPort}",
@@ -398,8 +391,7 @@ public sealed class TwinCAT3SmokeTests(TwinCATXarFixture sim)
DeviceHostAddress: $"ads://{sim.TargetNetId}:{sim.AmsPort}",
SymbolPath: "GVL_Fixture.nCounter",
DataType: TwinCATDataType.DInt,
Writable: false),
],
Writable: false)),
Timeout = TimeSpan.FromSeconds(5),
Probe = new TwinCATProbeOptions { Enabled = false },
};
@@ -442,8 +434,7 @@ public sealed class TwinCAT3SmokeTests(TwinCATXarFixture sim)
new TwinCATDeviceOptions(realHost, "Real-VM"),
new TwinCATDeviceOptions(ghostHost, "Ghost-VM"),
],
Tags =
[
RawTags = TwinCATRawTags.Entries(
new TwinCATTagDefinition(
Name: "RealCounter",
DeviceHostAddress: realHost,
@@ -453,8 +444,7 @@ public sealed class TwinCAT3SmokeTests(TwinCATXarFixture sim)
Name: "GhostCounter",
DeviceHostAddress: ghostHost,
SymbolPath: "GVL_Fixture.nCounter",
DataType: TwinCATDataType.DInt),
],
DataType: TwinCATDataType.DInt)),
// Shorter timeout so the bogus device fails fast rather than dragging the whole
// test; the healthy read shouldn't be slowed down by a peer timeout.
Timeout = TimeSpan.FromSeconds(2),
@@ -488,7 +478,7 @@ public sealed class TwinCAT3SmokeTests(TwinCATXarFixture sim)
var options = new TwinCATDriverOptions
{
Devices = [new TwinCATDeviceOptions($"ads://{sim.TargetNetId}:{sim.AmsPort}", "XAR-VM")],
Tags = [],
RawTags = [],
Timeout = TimeSpan.FromSeconds(5),
Probe = new TwinCATProbeOptions
{
@@ -532,7 +522,7 @@ public sealed class TwinCAT3SmokeTests(TwinCATXarFixture sim)
var options = new TwinCATDriverOptions
{
Devices = baseOptions.Devices,
Tags = baseOptions.Tags,
RawTags = baseOptions.RawTags,
UseNativeNotifications = baseOptions.UseNativeNotifications,
Timeout = baseOptions.Timeout,
Probe = baseOptions.Probe,
@@ -565,7 +555,7 @@ public sealed class TwinCAT3SmokeTests(TwinCATXarFixture sim)
HostAddress: $"ads://{sim.TargetNetId}:{sim.AmsPort}",
DeviceName: "XAR-VM"),
],
Tags = [
RawTags = TwinCATRawTags.Entries(
new TwinCATTagDefinition(
Name: "Counter",
DeviceHostAddress: $"ads://{sim.TargetNetId}:{sim.AmsPort}",
@@ -582,8 +572,7 @@ public sealed class TwinCAT3SmokeTests(TwinCATXarFixture sim)
DeviceHostAddress: $"ads://{sim.TargetNetId}:{sim.AmsPort}",
SymbolPath: "GVL_Arrays.aReal1D[5]",
DataType: TwinCATDataType.Real,
Writable: true),
],
Writable: true)),
UseNativeNotifications = true,
Timeout = TimeSpan.FromSeconds(5),
// Disable the probe loop — the smoke tests run their own reads; a background
@@ -0,0 +1,32 @@
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests;
/// <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 integration smoke 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. Mirrors the unit-test project's helper of the same name.
/// </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 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 TwinCATTagDefinition[] defs)
=> [.. defs.Select(Entry)];
}