fix(driver-ablegacy-cli): resolve Low code-review findings (Driver.AbLegacy.Cli-002,003,004,005,006,007)
- Driver.AbLegacy.Cli-002: WriteCommand.Value description lists the full true/false, 1/0, on/off, yes/no alias set. - Driver.AbLegacy.Cli-003: SubscribeCommand serialises every WriteLine via a per-execution consoleGate lock so the poll-thread OnDataChange handler can't interleave with the banner. - Driver.AbLegacy.Cli-004: dropped 'await using var driver' in favour of a plain 'var driver' + explicit await ShutdownAsync in finally; the driver is no longer shut down twice. - Driver.AbLegacy.Cli-005: SubscribeCommand.IntervalMs description carries the PollGroupEngine 250ms-floor caveat; docs/Driver.AbLegacy.Cli.md spells out the same. - Driver.AbLegacy.Cli-006: ProbeCommand --type now carries the short alias 't' to match the other commands. - Driver.AbLegacy.Cli-007: BuildOptionsTests cover the probe-disabled, device-shape, tag-passthrough, timeout-propagation, and empty-tag-list paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
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
|
||||
{
|
||||
public AbLegacyDriverOptions Build(IReadOnlyList<AbLegacyTagDefinition> tags)
|
||||
=> BuildOptions(tags);
|
||||
|
||||
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),
|
||||
];
|
||||
|
||||
[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.");
|
||||
}
|
||||
|
||||
[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");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildOptions_forwards_tag_list_verbatim()
|
||||
{
|
||||
var cmd = new TestCommand
|
||||
{
|
||||
Gateway = "ab://192.168.1.20/1,0",
|
||||
PlcType = AbLegacyPlcFamily.Slc500,
|
||||
TimeoutMs = 5000,
|
||||
};
|
||||
|
||||
var options = cmd.Build(SampleTags);
|
||||
|
||||
options.Tags.ShouldBe(SampleTags);
|
||||
}
|
||||
|
||||
[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));
|
||||
}
|
||||
|
||||
[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.Tags.ShouldBeEmpty();
|
||||
options.Devices.Count.ShouldBe(1);
|
||||
options.Devices[0].PlcFamily.ShouldBe(AbLegacyPlcFamily.Plc5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using CliFx.Attributes;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Commands;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Locks in the CLI command-option contract surface area — short aliases and
|
||||
/// help-text wording — that the AbLegacy CLI is expected to keep in parity with
|
||||
/// its sibling AbCip CLI and with <c>docs/Driver.AbLegacy.Cli.md</c>.
|
||||
/// Regression coverage for findings Driver.AbLegacy.Cli-002, -005, -006.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class CommandMetadataTests
|
||||
{
|
||||
private static CommandOptionAttribute GetOption<TCommand>(string propertyName)
|
||||
{
|
||||
var prop = typeof(TCommand).GetProperty(
|
||||
propertyName,
|
||||
BindingFlags.Public | BindingFlags.Instance);
|
||||
prop.ShouldNotBeNull($"property {propertyName} is missing from {typeof(TCommand).Name}");
|
||||
var attr = prop!.GetCustomAttribute<CommandOptionAttribute>();
|
||||
attr.ShouldNotBeNull(
|
||||
$"property {propertyName} on {typeof(TCommand).Name} lacks [CommandOption]");
|
||||
return attr!;
|
||||
}
|
||||
|
||||
// ---------- Driver.AbLegacy.Cli-006 — ProbeCommand --type needs short alias 't' ----------
|
||||
|
||||
[Fact]
|
||||
public void ProbeCommand_type_has_short_alias_t()
|
||||
{
|
||||
// Parity with read / write / subscribe: --type / -t works everywhere.
|
||||
var attr = GetOption<ProbeCommand>(nameof(ProbeCommand.DataType));
|
||||
attr.ShortName.ShouldBe('t');
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(ReadCommand), nameof(ReadCommand.DataType))]
|
||||
[InlineData(typeof(WriteCommand), nameof(WriteCommand.DataType))]
|
||||
[InlineData(typeof(SubscribeCommand), nameof(SubscribeCommand.DataType))]
|
||||
public void Other_commands_keep_type_short_alias_t(System.Type commandType, string propName)
|
||||
{
|
||||
var prop = commandType.GetProperty(propName, BindingFlags.Public | BindingFlags.Instance);
|
||||
prop.ShouldNotBeNull();
|
||||
var attr = prop!.GetCustomAttribute<CommandOptionAttribute>();
|
||||
attr.ShouldNotBeNull();
|
||||
attr!.ShortName.ShouldBe('t');
|
||||
}
|
||||
|
||||
// ---------- Driver.AbLegacy.Cli-002 — WriteCommand --value help lists full bool alias set ----------
|
||||
|
||||
[Fact]
|
||||
public void WriteCommand_value_help_lists_full_boolean_alias_set()
|
||||
{
|
||||
// ParseBool accepts true/false, 1/0, on/off, yes/no — the help text must say so
|
||||
// (DriverClis.md documents the full alias set as the shared CLI contract).
|
||||
var attr = GetOption<WriteCommand>(nameof(WriteCommand.Value));
|
||||
attr.Description.ShouldNotBeNull();
|
||||
attr.Description!.ShouldContain("true/false", Case.Insensitive);
|
||||
attr.Description!.ShouldContain("1/0");
|
||||
attr.Description!.ShouldContain("on/off", Case.Insensitive);
|
||||
attr.Description!.ShouldContain("yes/no", Case.Insensitive);
|
||||
}
|
||||
|
||||
// ---------- Driver.AbLegacy.Cli-005 — SubscribeCommand --interval-ms help notes 250ms floor ----------
|
||||
|
||||
[Fact]
|
||||
public void SubscribeCommand_interval_ms_help_notes_PollGroupEngine_floor()
|
||||
{
|
||||
// Parity with AbCip CLI: operators passing -i 100 deserve a heads-up that
|
||||
// PollGroupEngine floors sub-250ms values.
|
||||
var attr = GetOption<SubscribeCommand>(nameof(SubscribeCommand.IntervalMs));
|
||||
attr.Description.ShouldNotBeNull();
|
||||
attr.Description!.ShouldContain("250", Case.Insensitive);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user