Merge B1-wp6integ
This commit is contained in:
@@ -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))];
|
||||
}
|
||||
+1
-1
@@ -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}");
|
||||
|
||||
|
||||
+2
-3
@@ -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");
|
||||
|
||||
+2
-3
@@ -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)];
|
||||
}
|
||||
+4
-6
@@ -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)];
|
||||
}
|
||||
+7
-11
@@ -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
|
||||
{
|
||||
|
||||
+4
-4
@@ -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
|
||||
{
|
||||
|
||||
+2
-2
@@ -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");
|
||||
|
||||
+2
-3
@@ -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");
|
||||
|
||||
+1
-1
@@ -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 },
|
||||
};
|
||||
}
|
||||
|
||||
+2
-3
@@ -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");
|
||||
|
||||
+2
-3
@@ -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");
|
||||
|
||||
+2
-3
@@ -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 },
|
||||
|
||||
+2
-3
@@ -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");
|
||||
|
||||
+4
-6
@@ -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");
|
||||
|
||||
+1
-1
@@ -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 },
|
||||
};
|
||||
}
|
||||
|
||||
+4
-6
@@ -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");
|
||||
|
||||
+2
-3
@@ -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 },
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
@@ -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 },
|
||||
|
||||
+6
-9
@@ -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)];
|
||||
}
|
||||
+2
-4
@@ -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)),
|
||||
};
|
||||
}
|
||||
|
||||
+14
-25
@@ -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)];
|
||||
}
|
||||
Reference in New Issue
Block a user