feat(abcip): Transport option selects native or libplctag across driver, probe and CLI
This commit is contained in:
+55
@@ -0,0 +1,55 @@
|
||||
using CliFx;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.Tests;
|
||||
|
||||
/// <summary>The CLI <c>--transport</c> flag reaches <see cref="AbCipDriverOptions.Transport"/>.</summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class AbCipTransportOptionTests
|
||||
{
|
||||
[Command("capture-transport")]
|
||||
private sealed class CapturingCommand : AbCipCommandBase
|
||||
{
|
||||
public static AbCipTransport? Captured;
|
||||
|
||||
public override ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
Captured = BuildOptions([]).Transport;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies the default is libplctag when the flag is absent.</summary>
|
||||
[Fact]
|
||||
public async Task Transport_defaults_to_libplctag()
|
||||
{
|
||||
CapturingCommand.Captured = null;
|
||||
using var console = new FakeInMemoryConsole();
|
||||
var app = new CliApplicationBuilder().AddCommand(typeof(CapturingCommand)).UseConsole(console).Build();
|
||||
|
||||
var exit = await app.RunAsync(["capture-transport", "--gateway", "ab://1.2.3.4/1,0"]);
|
||||
|
||||
exit.ShouldBe(0, console.ReadErrorString());
|
||||
CapturingCommand.Captured.ShouldBe(AbCipTransport.Libplctag);
|
||||
}
|
||||
|
||||
/// <summary>Verifies CliFx parses <c>--transport</c> (case-insensitive) into the options.</summary>
|
||||
[Theory]
|
||||
[InlineData("native", AbCipTransport.Native)]
|
||||
[InlineData("Native", AbCipTransport.Native)]
|
||||
[InlineData("libplctag", AbCipTransport.Libplctag)]
|
||||
public async Task Transport_flag_is_parsed_by_the_cli(string value, AbCipTransport expected)
|
||||
{
|
||||
CapturingCommand.Captured = null;
|
||||
using var console = new FakeInMemoryConsole();
|
||||
var app = new CliApplicationBuilder().AddCommand(typeof(CapturingCommand)).UseConsole(console).Build();
|
||||
|
||||
var exit = await app.RunAsync(["capture-transport", "--gateway", "ab://1.2.3.4/1,0", "--transport", value]);
|
||||
|
||||
exit.ShouldBe(0, console.ReadErrorString());
|
||||
CapturingCommand.Captured.ShouldBe(expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
||||
|
||||
/// <summary>The per-driver <c>Transport</c> switch: config parsing, driver factory selection and the probe.</summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class AbCipTransportSwitchTests
|
||||
{
|
||||
private const string Device = """{"hostAddress":"ab://10.0.0.1:44818/1,0","plcFamily":"ControlLogix"}""";
|
||||
|
||||
private static string Config(string? transportJson) =>
|
||||
$$"""{{{(transportJson is null ? "" : $"\"transport\":{transportJson},")}}"devices":[{{Device}}]}""";
|
||||
|
||||
/// <summary>Transport values select the factory trio; absent defaults to libplctag.</summary>
|
||||
[Theory]
|
||||
[InlineData("\"Native\"", AbCipTransport.Native)]
|
||||
[InlineData("\"native\"", AbCipTransport.Native)]
|
||||
[InlineData("\"Libplctag\"", AbCipTransport.Libplctag)]
|
||||
[InlineData(null, AbCipTransport.Libplctag)]
|
||||
public void Config_transport_selects_the_factory(string? json, AbCipTransport expected)
|
||||
{
|
||||
AbCipDriverFactoryExtensions.ParseOptions("ab1", Config(json)).Transport.ShouldBe(expected);
|
||||
|
||||
var driver = AbCipDriverFactoryExtensions.CreateInstance("ab1", Config(json));
|
||||
if (expected == AbCipTransport.Native)
|
||||
driver.TagFactory.ShouldBeOfType<NativeAbCipTagFactory>();
|
||||
else
|
||||
driver.TagFactory.ShouldBeOfType<LibplctagTagFactory>();
|
||||
}
|
||||
|
||||
/// <summary>The factory trio matches the transport for all three factory kinds.</summary>
|
||||
[Fact]
|
||||
public void Factories_For_returns_a_matching_trio()
|
||||
{
|
||||
var n = AbCipTransportFactories.For(AbCipTransport.Native);
|
||||
n.Tags.ShouldBeOfType<NativeAbCipTagFactory>();
|
||||
n.Enumerators.ShouldBeOfType<NativeAbCipTagEnumeratorFactory>();
|
||||
n.Templates.ShouldBeOfType<NativeAbCipTemplateReaderFactory>();
|
||||
|
||||
var l = AbCipTransportFactories.For(AbCipTransport.Libplctag);
|
||||
l.Tags.ShouldBeOfType<LibplctagTagFactory>();
|
||||
l.Enumerators.ShouldBeOfType<LibplctagTagEnumeratorFactory>();
|
||||
l.Templates.ShouldBeOfType<LibplctagTemplateReaderFactory>();
|
||||
}
|
||||
|
||||
/// <summary>A bad value fails the way an existing bad enum field (PlcFamily) does.</summary>
|
||||
[Fact]
|
||||
public void Bad_transport_fails_like_a_bad_PlcFamily()
|
||||
{
|
||||
var bad = Should.Throw<InvalidOperationException>(
|
||||
() => AbCipDriverFactoryExtensions.ParseOptions("ab1", Config("\"Bogus\"")));
|
||||
bad.Message.ShouldContain("unknown Transport 'Bogus'");
|
||||
|
||||
var existing = Should.Throw<InvalidOperationException>(() => AbCipDriverFactoryExtensions.ParseOptions(
|
||||
"ab1", """{"devices":[{"hostAddress":"ab://10.0.0.1/1,0","plcFamily":"Bogus"}]}"""));
|
||||
existing.GetType().ShouldBe(bad.GetType());
|
||||
existing.Message.ShouldContain("unknown PlcFamily 'Bogus'");
|
||||
}
|
||||
|
||||
/// <summary>An explicitly injected factory wins over the option.</summary>
|
||||
[Fact]
|
||||
public void Injected_factory_wins_over_the_option()
|
||||
{
|
||||
var fake = new FakeAbCipTagFactory();
|
||||
var driver = new AbCipDriver(new AbCipDriverOptions { Transport = AbCipTransport.Native }, "ab1", fake);
|
||||
driver.TagFactory.ShouldBeSameAs(fake);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reinit does NOT swap transports (by design): the factories are constructor-held, so a changed
|
||||
/// <c>transport</c> in the reinit JSON is inert. A deploy recreates the instance instead (#516).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Reinit_with_a_changed_transport_keeps_the_constructed_transport()
|
||||
{
|
||||
var driver = AbCipDriverFactoryExtensions.CreateInstance("ab1", Config("\"Libplctag\""));
|
||||
var before = driver.TagFactory;
|
||||
|
||||
await driver.ReinitializeAsync(Config("\"Native\""), TestContext.Current.CancellationToken);
|
||||
|
||||
driver.TagFactory.ShouldBeSameAs(before);
|
||||
driver.TagFactory.ShouldBeOfType<LibplctagTagFactory>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The probe's Phase 1 TCP preflight rejects a refused port before any runtime exists, so the native
|
||||
/// path is proven against a listener that accepts TCP and hangs up: only a runtime that really opens
|
||||
/// an EtherNet/IP session reports the native transport's own failure text.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Probe_with_Native_uses_the_native_path()
|
||||
{
|
||||
using var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try { (await listener.AcceptTcpClientAsync(TestContext.Current.CancellationToken)).Dispose(); }
|
||||
catch { return; }
|
||||
}
|
||||
}, TestContext.Current.CancellationToken);
|
||||
|
||||
string Cfg(string t) =>
|
||||
$$"""{"transport":"{{t}}","devices":[{"hostAddress":"ab://127.0.0.1:{{port}}/1,0"}]}""";
|
||||
var probe = new AbCipDriverProbe();
|
||||
|
||||
var native = await probe.ProbeAsync(Cfg("Native"), TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
|
||||
var lib = await probe.ProbeAsync(Cfg("Libplctag"), TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
|
||||
listener.Stop();
|
||||
|
||||
native.Ok.ShouldBeFalse();
|
||||
native.Message.ShouldNotBeNull();
|
||||
native.Message!.ShouldContain("CIP handshake failed: Connection lost");
|
||||
lib.Message.ShouldNotContain("Connection lost");
|
||||
}
|
||||
|
||||
/// <summary>The probe deserialises with its own string-enum converter; a bad transport is a config error.</summary>
|
||||
[Fact]
|
||||
public async Task Probe_with_a_bad_transport_reports_invalid_config()
|
||||
{
|
||||
var r = await new AbCipDriverProbe().ProbeAsync(
|
||||
Config("\"Bogus\""), TimeSpan.FromSeconds(2), TestContext.Current.CancellationToken);
|
||||
r.Ok.ShouldBeFalse();
|
||||
r.Message!.ShouldContain("Config JSON is invalid");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user