using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using CliFx.Infrastructure;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Tests;
///
/// Locks in : probe disabled,
/// device shape populated from --gateway + --plc-type, tag list
/// forwarded verbatim, and timeout propagated from --timeout-ms. A
/// regression here silently changes every AbLegacy CLI command's behaviour, so
/// covering it explicitly closes the test gap called out by finding
/// Driver.AbLegacy.Cli-007.
///
[Trait("Category", "Unit")]
public sealed class BuildOptionsTests
{
// Concrete subclass needed because AbLegacyCommandBase is abstract. Exposes the
// protected BuildOptions via a public surface for the test.
// [Command] satisfies CliFx's analyzer (ICommand subtypes must be annotated);
// we never run it through CliFx, only invoke Build() directly.
[Command("test-build-options")]
private sealed class TestCommand : AbLegacyCommandBase
{
/// Builds driver options from the command's configuration and the provided tags.
/// The tag definitions to include in the options.
/// Configured driver options.
public AbLegacyDriverOptions Build(IReadOnlyList tags)
=> BuildOptions(tags);
/// Not used; this test command is for BuildOptions inspection only.
/// Not used.
/// Not used.
/// Always thrown; this method should not be called.
public override System.Threading.Tasks.ValueTask ExecuteAsync(IConsole console)
=> throw new NotSupportedException("TestCommand is for BuildOptions inspection only.");
}
private static readonly IReadOnlyList SampleTags =
[
new AbLegacyTagDefinition(
Name: "N7:0:Int",
DeviceHostAddress: "ab://192.168.1.20/1,0",
Address: "N7:0",
DataType: AbLegacyDataType.Int,
Writable: false),
new AbLegacyTagDefinition(
Name: "F8:0:Float",
DeviceHostAddress: "ab://192.168.1.20/1,0",
Address: "F8:0",
DataType: AbLegacyDataType.Float,
Writable: true),
];
/// Verifies that probe is disabled for CLI one-shot runs.
[Fact]
public void BuildOptions_disables_probe_for_cli_oneshot_runs()
{
var cmd = new TestCommand
{
Gateway = "ab://192.168.1.20/1,0",
PlcType = AbLegacyPlcFamily.Slc500,
TimeoutMs = 5000,
};
var options = cmd.Build(SampleTags);
options.Probe.ShouldNotBeNull();
options.Probe.Enabled.ShouldBeFalse(
"CLI commands are one-shot; the background probe loop is unwanted overhead.");
}
/// Verifies that device is populated from gateway and plc-type options.
[Fact]
public void BuildOptions_populates_single_device_from_gateway_and_plc_type()
{
var cmd = new TestCommand
{
Gateway = "ab://10.0.0.5/1,0",
PlcType = AbLegacyPlcFamily.MicroLogix,
TimeoutMs = 5000,
};
var options = cmd.Build(SampleTags);
options.Devices.Count.ShouldBe(1);
options.Devices[0].HostAddress.ShouldBe("ab://10.0.0.5/1,0");
options.Devices[0].PlcFamily.ShouldBe(AbLegacyPlcFamily.MicroLogix);
options.Devices[0].DeviceName.ShouldBe("cli-MicroLogix");
}
///
/// Verifies that the typed tag list is delivered as v3 records: RawPath =
/// the def's Name, DeviceName = the def's DeviceHostAddress, and the TagConfig blob round-trips the
/// def's address / dataType / writable through the real mapper.
///
[Fact]
public void BuildOptions_delivers_tags_as_rawtag_entries()
{
var cmd = new TestCommand
{
Gateway = "ab://192.168.1.20/1,0",
PlcType = AbLegacyPlcFamily.Slc500,
TimeoutMs = 5000,
};
var options = cmd.Build(SampleTags);
options.RawTags.Count.ShouldBe(SampleTags.Count);
for (var i = 0; i < SampleTags.Count; i++)
{
var def = SampleTags[i];
var entry = options.RawTags[i];
entry.RawPath.ShouldBe(def.Name);
entry.DeviceName.ShouldBe(def.DeviceHostAddress);
AbLegacyTagDefinitionFactory.FromTagConfig(entry.TagConfig, entry.RawPath, out var round).ShouldBeTrue();
round.Address.ShouldBe(def.Address);
round.DataType.ShouldBe(def.DataType);
round.Writable.ShouldBe(def.Writable);
}
}
/// Verifies that timeout-ms option is propagated to the driver options.
[Fact]
public void BuildOptions_propagates_timeout_ms()
{
var cmd = new TestCommand
{
Gateway = "ab://192.168.1.20/1,0",
PlcType = AbLegacyPlcFamily.Slc500,
TimeoutMs = 7500,
};
var options = cmd.Build(SampleTags);
options.Timeout.ShouldBe(TimeSpan.FromMilliseconds(7500));
}
/// Verifies that empty tag list yields an empty tags collection in options.
[Fact]
public void BuildOptions_with_empty_tag_list_yields_empty_tags_collection()
{
var cmd = new TestCommand
{
Gateway = "ab://192.168.1.20/1,0",
PlcType = AbLegacyPlcFamily.Plc5,
TimeoutMs = 5000,
};
var options = cmd.Build([]);
options.RawTags.ShouldBeEmpty();
options.Devices.Count.ShouldBe(1);
options.Devices[0].PlcFamily.ShouldBe(AbLegacyPlcFamily.Plc5);
}
///
/// With no --transport the CLI leaves the option null and the driver's resolver decides: libplctag
/// for every family (final review I3; Native is opt-in). An explicit choice is forwarded verbatim and wins.
///
[Theory]
[InlineData(AbLegacyPlcFamily.Slc500, AbLegacyTransport.Libplctag)]
[InlineData(AbLegacyPlcFamily.MicroLogix, AbLegacyTransport.Libplctag)]
[InlineData(AbLegacyPlcFamily.Plc5, AbLegacyTransport.Libplctag)]
[InlineData(AbLegacyPlcFamily.LogixPccc, AbLegacyTransport.Libplctag)]
public void BuildOptions_default_transport_goes_through_the_resolver(AbLegacyPlcFamily family, AbLegacyTransport expected)
{
var options = new TestCommand { Gateway = "ab://192.168.1.20/1,0", PlcType = family }.Build([]);
options.Transport.ShouldBeNull();
AbLegacyTransportResolver.Resolve(options).ShouldBe(expected);
var native = new TestCommand
{
Gateway = "ab://192.168.1.20/1,0", PlcType = family, Transport = AbLegacyCliTransport.Native,
}.Build([]);
native.Transport.ShouldBe(AbLegacyTransport.Native);
AbLegacyTransportResolver.Resolve(native).ShouldBe(AbLegacyTransport.Native);
}
///
/// End to end through CliFx: no --transport resolves to libplctag for every --plc-type
/// (final review I3). The slc500 / micrologix rows are the falsification rows — they resolved Native
/// before the revert.
///
[Theory]
[InlineData("plc5", AbLegacyTransport.Libplctag)]
[InlineData("logixpccc", AbLegacyTransport.Libplctag)]
[InlineData("slc500", AbLegacyTransport.Libplctag)]
[InlineData("micrologix", AbLegacyTransport.Libplctag)]
public async Task Plc_type_without_transport_flag_resolves_through_the_resolver(string plcType, AbLegacyTransport expected)
{
CapturingCommand.Options = 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", "--plc-type", plcType]);
exit.ShouldBe(0, console.ReadErrorString());
CapturingCommand.Options.ShouldNotBeNull();
CapturingCommand.Options.Transport.ShouldBeNull();
AbLegacyTransportResolver.Resolve(CapturingCommand.Options).ShouldBe(expected);
}
/// Verifies CliFx really parses --transport (case-insensitive) into the property.
[Theory]
[InlineData("native", AbLegacyTransport.Native)]
[InlineData("Native", AbLegacyTransport.Native)]
[InlineData("libplctag", AbLegacyTransport.Libplctag)]
[InlineData("default", null)]
public async Task Transport_flag_is_parsed_by_the_cli(string value, AbLegacyTransport? expected)
{
CapturingCommand.Options = 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.Options.ShouldNotBeNull();
CapturingCommand.Options.Transport.ShouldBe(expected);
}
[Command("capture-transport")]
private sealed class CapturingCommand : AbLegacyCommandBase
{
public static AbLegacyDriverOptions? Options;
public override System.Threading.Tasks.ValueTask ExecuteAsync(IConsole console)
{
Options = BuildOptions([]);
return default;
}
}
}