ed0881d672
Task 22 (spec section 8 P5), with the controller rulings that exclude PLC-5 and LogixPccc. - Transport is nullable on both option records; null (key absent) means "use the default" and is never serialized. Factory DTO parse: absent -> null; a bad value still fails via ParseEnum. - One resolver per driver (AbCipTransportResolver / AbLegacyTransportResolver), used by the driver constructor, the Test Connect probe and (through the driver) the CLI. AbCip: null -> Native. AbLegacy: null -> Native unless any device is Plc5 or LogixPccc, then the whole driver resolves to Libplctag. Explicit values always win; explicit Native with a Plc5/LogixPccc device logs one Warning at init. - Init log shows configured vs effective, e.g. transport=Native (default). - AdminUI forms: "Default (...)" / "Native (managed)" / "libplctag (fallback)". Default omits the key on save (reverses Task 19's explicit re-emit, which would have pinned every edited driver). - CLIs: --transport default|native|libplctag, default "default" via the resolver, so --plc-type Plc5 with no --transport runs libplctag. - Tests pinned to Libplctag where a default-path runtime was created: found by instrumenting the native factories with the current xUnit test name (one unit test, AbLegacyReinitConfigAdoption; plus the two fixture-gated AbCip probe E2E tests whose exact messages were proven on libplctag). TransportMatrix and the differential harness already pass explicit transports. - Switch/resolver/CLI/form tests are falsified by mutating the resolver (default -> Libplctag, exclusion removed, AbLegacy default -> Libplctag): each mutation turns them red. - Replaces the inline role-name literal in AbCipDriverFactoryExtensions' ParseEnum call with a named constant; RoleAuthorityGuardTests was already red on the base commit for that line. - Docs: AbCip.md / AbLegacy.md default + release note; CLAUDE.md AB section. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
241 lines
9.7 KiB
C#
241 lines
9.7 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Locks in <see cref="AbLegacyCommandBase.BuildOptions"/>: probe disabled,
|
|
/// device shape populated from <c>--gateway</c> + <c>--plc-type</c>, tag list
|
|
/// forwarded verbatim, and timeout propagated from <c>--timeout-ms</c>. 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.
|
|
/// </summary>
|
|
[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
|
|
{
|
|
/// <summary>Builds driver options from the command's configuration and the provided tags.</summary>
|
|
/// <param name="tags">The tag definitions to include in the options.</param>
|
|
/// <returns>Configured driver options.</returns>
|
|
public AbLegacyDriverOptions Build(IReadOnlyList<AbLegacyTagDefinition> tags)
|
|
=> BuildOptions(tags);
|
|
|
|
/// <summary>Not used; this test command is for BuildOptions inspection only.</summary>
|
|
/// <param name="console">Not used.</param>
|
|
/// <returns>Not used.</returns>
|
|
/// <exception cref="NotSupportedException">Always thrown; this method should not be called.</exception>
|
|
public override System.Threading.Tasks.ValueTask ExecuteAsync(IConsole console)
|
|
=> throw new NotSupportedException("TestCommand is for BuildOptions inspection only.");
|
|
}
|
|
|
|
private static readonly IReadOnlyList<AbLegacyTagDefinition> 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),
|
|
];
|
|
|
|
/// <summary>Verifies that probe is disabled for CLI one-shot runs.</summary>
|
|
[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.");
|
|
}
|
|
|
|
/// <summary>Verifies that device is populated from gateway and plc-type options.</summary>
|
|
[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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the typed tag list is delivered as v3 <see cref="RawTagEntry"/> 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.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
}
|
|
|
|
/// <summary>Verifies that timeout-ms option is propagated to the driver options.</summary>
|
|
[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));
|
|
}
|
|
|
|
/// <summary>Verifies that empty tag list yields an empty tags collection in options.</summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// With no <c>--transport</c> the CLI leaves the option null and the driver's resolver decides: Native
|
|
/// for an SLC 500, libplctag for <c>--plc-type Plc5</c> / LogixPccc (Task 22 exclusion). An explicit
|
|
/// choice is forwarded verbatim and wins.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData(AbLegacyPlcFamily.Slc500, AbLegacyTransport.Native)]
|
|
[InlineData(AbLegacyPlcFamily.MicroLogix, AbLegacyTransport.Native)]
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The plan's CLI case end to end through CliFx: <c>--plc-type plc5</c> with no <c>--transport</c>
|
|
/// resolves to libplctag; the same invocation for an SLC resolves Native (falsification control).
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("plc5", AbLegacyTransport.Libplctag)]
|
|
[InlineData("logixpccc", AbLegacyTransport.Libplctag)]
|
|
[InlineData("slc500", AbLegacyTransport.Native)]
|
|
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);
|
|
}
|
|
|
|
/// <summary>Verifies CliFx really parses <c>--transport</c> (case-insensitive) into the property.</summary>
|
|
[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;
|
|
}
|
|
}
|
|
}
|