@@ -0,0 +1,375 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbCip.PlcFamilies;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// PR abcip-3.2 — coverage for the per-device <c>AddressingMode</c> toggle.
|
||||
/// Asserts (a) <see cref="AddressingMode.Auto"/> resolves to
|
||||
/// <see cref="AddressingMode.Symbolic"/> at the device level, (b) explicit
|
||||
/// <see cref="AddressingMode.Logical"/> threads through every
|
||||
/// <see cref="AbCipTagCreateParams"/> the driver builds, (c) Logical against an unsupported
|
||||
/// family (Micro800) emits a warning + falls back to Symbolic, (d) the Driver-config DTO
|
||||
/// round-trips the mode, and (e) family compatibility is captured by
|
||||
/// <see cref="AbCipPlcFamilyProfile.SupportsLogicalAddressing"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class AbCipAddressingModeTests
|
||||
{
|
||||
// ---- Auto resolves to Symbolic ----
|
||||
|
||||
[Fact]
|
||||
public async Task Default_AddressingMode_resolves_to_Symbolic_on_DeviceState()
|
||||
{
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0", AbCipPlcFamily.ControlLogix)],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
}, "drv-1");
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Symbolic);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Auto_AddressingMode_resolves_to_Symbolic_on_DeviceState()
|
||||
{
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.5/1,0",
|
||||
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||
AddressingMode: AddressingMode.Auto),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
}, "drv-1");
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Symbolic);
|
||||
}
|
||||
|
||||
// ---- Logical threads through to AbCipTagCreateParams ----
|
||||
|
||||
[Fact]
|
||||
public async Task Logical_AddressingMode_threads_through_into_create_params()
|
||||
{
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.5/1,0",
|
||||
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||
AddressingMode: AddressingMode.Logical),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
Tags =
|
||||
[
|
||||
new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt),
|
||||
],
|
||||
}, "drv-1", tagFactory: factory,
|
||||
enumeratorFactory: new EmptyEnumeratorFactoryStub());
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||
|
||||
factory.Tags["Speed"].CreationParams.AddressingMode.ShouldBe(AddressingMode.Logical);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Symbolic_AddressingMode_explicitly_set_threads_through()
|
||||
{
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.5/1,0",
|
||||
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||
AddressingMode: AddressingMode.Symbolic),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
Tags =
|
||||
[
|
||||
new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt),
|
||||
],
|
||||
}, "drv-1", tagFactory: factory);
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||
|
||||
factory.Tags["Speed"].CreationParams.AddressingMode.ShouldBe(AddressingMode.Symbolic);
|
||||
}
|
||||
|
||||
// ---- Logical against unsupported family falls back with warning ----
|
||||
|
||||
[Fact]
|
||||
public async Task Logical_on_Micro800_falls_back_to_Symbolic_with_warning()
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.6/",
|
||||
PlcFamily: AbCipPlcFamily.Micro800,
|
||||
AddressingMode: AddressingMode.Logical),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
OnWarning = warnings.Add,
|
||||
}, "drv-1");
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
drv.GetDeviceState("ab://10.0.0.6/")!.AddressingMode.ShouldBe(AddressingMode.Symbolic);
|
||||
warnings.ShouldHaveSingleItem();
|
||||
warnings[0].ShouldContain("Micro800");
|
||||
warnings[0].ShouldContain("Logical");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Logical_on_Micro800_carries_Symbolic_into_create_params()
|
||||
{
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.6/",
|
||||
PlcFamily: AbCipPlcFamily.Micro800,
|
||||
AddressingMode: AddressingMode.Logical),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
Tags =
|
||||
[
|
||||
new AbCipTagDefinition("Speed", "ab://10.0.0.6/", "Speed", AbCipDataType.DInt),
|
||||
],
|
||||
OnWarning = _ => { },
|
||||
}, "drv-1", tagFactory: factory);
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||
|
||||
factory.Tags["Speed"].CreationParams.AddressingMode.ShouldBe(AddressingMode.Symbolic);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Logical_on_ControlLogix_does_not_warn()
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.5/1,0",
|
||||
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||
AddressingMode: AddressingMode.Logical),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
OnWarning = warnings.Add,
|
||||
}, "drv-1");
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
warnings.ShouldBeEmpty();
|
||||
drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Logical);
|
||||
}
|
||||
|
||||
// ---- Family-profile compatibility flags ----
|
||||
|
||||
[Fact]
|
||||
public void Family_profiles_advertise_logical_support_correctly()
|
||||
{
|
||||
AbCipPlcFamilyProfile.ControlLogix.SupportsLogicalAddressing.ShouldBeTrue();
|
||||
AbCipPlcFamilyProfile.CompactLogix.SupportsLogicalAddressing.ShouldBeTrue();
|
||||
AbCipPlcFamilyProfile.GuardLogix.SupportsLogicalAddressing.ShouldBeTrue();
|
||||
AbCipPlcFamilyProfile.Micro800.SupportsLogicalAddressing.ShouldBeFalse();
|
||||
}
|
||||
|
||||
// ---- DTO round-trip ----
|
||||
|
||||
[Fact]
|
||||
public async Task DTO_round_trips_AddressingMode_Logical_through_config_json()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"Devices": [
|
||||
{
|
||||
"HostAddress": "ab://10.0.0.5/1,0",
|
||||
"PlcFamily": "ControlLogix",
|
||||
"AddressingMode": "Logical"
|
||||
}
|
||||
],
|
||||
"Probe": { "Enabled": false }
|
||||
}
|
||||
""";
|
||||
var drv = AbCipDriverFactoryExtensions.CreateInstance("drv-1", json);
|
||||
await drv.InitializeAsync(json, CancellationToken.None);
|
||||
drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Logical);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DTO_round_trips_AddressingMode_Symbolic_through_config_json()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"Devices": [
|
||||
{
|
||||
"HostAddress": "ab://10.0.0.5/1,0",
|
||||
"PlcFamily": "ControlLogix",
|
||||
"AddressingMode": "Symbolic"
|
||||
}
|
||||
],
|
||||
"Probe": { "Enabled": false }
|
||||
}
|
||||
""";
|
||||
var drv = AbCipDriverFactoryExtensions.CreateInstance("drv-1", json);
|
||||
await drv.InitializeAsync(json, CancellationToken.None);
|
||||
drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Symbolic);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DTO_omitted_AddressingMode_falls_back_to_Auto_then_Symbolic()
|
||||
{
|
||||
// No AddressingMode in JSON → DTO field is null → factory parses fallback Auto →
|
||||
// device-level resolution lands on Symbolic.
|
||||
var json = """
|
||||
{
|
||||
"Devices": [
|
||||
{
|
||||
"HostAddress": "ab://10.0.0.5/1,0",
|
||||
"PlcFamily": "ControlLogix"
|
||||
}
|
||||
],
|
||||
"Probe": { "Enabled": false }
|
||||
}
|
||||
""";
|
||||
var drv = AbCipDriverFactoryExtensions.CreateInstance("drv-1", json);
|
||||
await drv.InitializeAsync(json, CancellationToken.None);
|
||||
drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Symbolic);
|
||||
}
|
||||
|
||||
// ---- Logical-mode triggers a one-time symbol walk ----
|
||||
|
||||
[Fact]
|
||||
public async Task Logical_mode_first_read_triggers_symbol_walk_once()
|
||||
{
|
||||
var enumStub = new RecordingEnumeratorFactory(
|
||||
new AbCipDiscoveredTag("Speed", null, AbCipDataType.DInt, false),
|
||||
new AbCipDiscoveredTag("Counter", null, AbCipDataType.DInt, false));
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.5/1,0",
|
||||
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||
AddressingMode: AddressingMode.Logical),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
Tags =
|
||||
[
|
||||
new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt),
|
||||
new AbCipTagDefinition("Counter", "ab://10.0.0.5/1,0", "Counter", AbCipDataType.DInt),
|
||||
],
|
||||
}, "drv-1", tagFactory: factory, enumeratorFactory: enumStub);
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
// First read fires the walk
|
||||
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||
// Second read must NOT walk again
|
||||
await drv.ReadAsync(["Counter"], CancellationToken.None);
|
||||
|
||||
enumStub.CreateCount.ShouldBe(1);
|
||||
var device = drv.GetDeviceState("ab://10.0.0.5/1,0")!;
|
||||
device.LogicalWalkComplete.ShouldBeTrue();
|
||||
device.LogicalInstanceMap.ShouldContainKey("Speed");
|
||||
device.LogicalInstanceMap.ShouldContainKey("Counter");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Symbolic_mode_does_not_trigger_symbol_walk()
|
||||
{
|
||||
var enumStub = new RecordingEnumeratorFactory();
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.5/1,0",
|
||||
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||
AddressingMode: AddressingMode.Symbolic),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
Tags =
|
||||
[
|
||||
new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt),
|
||||
],
|
||||
}, "drv-1", tagFactory: factory, enumeratorFactory: enumStub);
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||
|
||||
enumStub.CreateCount.ShouldBe(0);
|
||||
drv.GetDeviceState("ab://10.0.0.5/1,0")!.LogicalWalkComplete.ShouldBeFalse();
|
||||
}
|
||||
|
||||
// ---- Stubs ----
|
||||
|
||||
private sealed class EmptyEnumeratorFactoryStub : IAbCipTagEnumeratorFactory
|
||||
{
|
||||
public IAbCipTagEnumerator Create() => new EmptyStub();
|
||||
|
||||
private sealed class EmptyStub : IAbCipTagEnumerator
|
||||
{
|
||||
public async IAsyncEnumerable<AbCipDiscoveredTag> EnumerateAsync(
|
||||
AbCipTagCreateParams deviceParams,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
yield break;
|
||||
}
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingEnumeratorFactory : IAbCipTagEnumeratorFactory
|
||||
{
|
||||
private readonly AbCipDiscoveredTag[] _seed;
|
||||
public int CreateCount;
|
||||
|
||||
public RecordingEnumeratorFactory(params AbCipDiscoveredTag[] seed) => _seed = seed;
|
||||
|
||||
public IAbCipTagEnumerator Create()
|
||||
{
|
||||
CreateCount++;
|
||||
return new SeededStub(_seed);
|
||||
}
|
||||
|
||||
private sealed class SeededStub(AbCipDiscoveredTag[] seed) : IAbCipTagEnumerator
|
||||
{
|
||||
public async IAsyncEnumerable<AbCipDiscoveredTag> EnumerateAsync(
|
||||
AbCipTagCreateParams deviceParams,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var tag in seed)
|
||||
yield return tag;
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user