using CliFx.Attributes; using CliFx.Infrastructure; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.Cli.Common; namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.Tests; /// /// Covers — the pure, deterministic mapping /// from the base's host / port / unit-id / timeout / disable-reconnect flags onto a /// ModbusDriverOptions. The CLI is one-shot so the background connectivity probe /// must be disabled; AutoReconnect is the inverse of --disable-reconnect. /// Also covers the input-range validation introduced for Driver.Modbus.Cli-003. /// [Trait("Category", "Unit")] public sealed class ModbusCommandBaseTests { // Test-only ModbusCommandBase concrete subclass that exposes the protected BuildOptions // helper + ValidateEndpoint. The [Command] attribute is required by the CliFx analyzer // (CliFx_CommandMustBeAnnotated) — this command is never registered with the CLI app // but the analyzer rule fires for every ICommand implementor in the compilation. [Command("noop-test", Description = "Test-only probe of ModbusCommandBase.BuildOptions.")] private sealed class ProbeOnly : ModbusCommandBase { public override ValueTask ExecuteAsync(IConsole console) => default; public ModbusDriverOptions Invoke(IReadOnlyList tags) => BuildOptions(tags); public void InvokeValidate() => ValidateEndpoint(); } [Fact] public void BuildOptions_disables_probe_for_one_shot_cli_runs() { var sut = new ProbeOnly { Host = "10.0.0.5" }; var options = sut.Invoke([]); options.Probe.ShouldNotBeNull(); options.Probe.Enabled.ShouldBeFalse(); } [Fact] public void BuildOptions_maps_TimeoutMs_to_Timeout_TimeSpan() { var sut = new ProbeOnly { Host = "h", TimeoutMs = 7500 }; var options = sut.Invoke([]); options.Timeout.ShouldBe(TimeSpan.FromMilliseconds(7500)); } [Fact] public void BuildOptions_AutoReconnect_defaults_to_true_when_flag_unset() { var sut = new ProbeOnly { Host = "h" }; var options = sut.Invoke([]); options.AutoReconnect.ShouldBeTrue(); } [Fact] public void BuildOptions_AutoReconnect_becomes_false_when_disable_reconnect_flag_set() { var sut = new ProbeOnly { Host = "h", DisableAutoReconnect = true }; var options = sut.Invoke([]); options.AutoReconnect.ShouldBeFalse(); } [Fact] public void BuildOptions_flows_host_port_unit_through() { var sut = new ProbeOnly { Host = "plc.shop.local", Port = 5020, UnitId = 17, TimeoutMs = 3000 }; var options = sut.Invoke([]); options.Host.ShouldBe("plc.shop.local"); options.Port.ShouldBe(5020); options.UnitId.ShouldBe((byte)17); } [Fact] public void BuildOptions_forwards_tag_list_verbatim() { var sut = new ProbeOnly { Host = "h" }; var tag = new ModbusTagDefinition( Name: "T", Region: ModbusRegion.HoldingRegisters, Address: 0, DataType: ModbusDataType.UInt16); var options = sut.Invoke([tag]); options.Tags.Count.ShouldBe(1); options.Tags[0].ShouldBeSameAs(tag); } // --- Driver.Modbus.Cli-003: parse-time endpoint validation ------------------------------- [Theory] [InlineData(0)] [InlineData(-1)] [InlineData(65536)] [InlineData(int.MinValue)] [InlineData(int.MaxValue)] public void ValidateEndpoint_rejects_port_outside_1_to_65535(int port) { var sut = new ProbeOnly { Host = "h", Port = port }; Should.Throw(() => sut.InvokeValidate()); } [Theory] [InlineData(1)] [InlineData(502)] [InlineData(65535)] public void ValidateEndpoint_accepts_port_in_range(int port) { var sut = new ProbeOnly { Host = "h", Port = port }; Should.NotThrow(() => sut.InvokeValidate()); } [Theory] [InlineData(0)] [InlineData(-1)] [InlineData(-2000)] public void ValidateEndpoint_rejects_non_positive_timeout(int timeoutMs) { var sut = new ProbeOnly { Host = "h", TimeoutMs = timeoutMs }; Should.Throw(() => sut.InvokeValidate()); } [Theory] [InlineData(0)] // broadcast — disallowed for unicast read/write requests [InlineData(248)] [InlineData(255)] public void ValidateEndpoint_rejects_unit_id_outside_1_to_247(byte unitId) { var sut = new ProbeOnly { Host = "h", UnitId = unitId }; Should.Throw(() => sut.InvokeValidate()); } [Theory] [InlineData(1)] [InlineData(247)] [InlineData(50)] public void ValidateEndpoint_accepts_unit_id_in_range(byte unitId) { var sut = new ProbeOnly { Host = "h", UnitId = unitId }; Should.NotThrow(() => sut.InvokeValidate()); } [Fact] public void ValidateEndpoint_accepts_default_options() { // Defaults: Port=502, UnitId=1, TimeoutMs=2000. All inside the valid ranges. var sut = new ProbeOnly { Host = "h" }; Should.NotThrow(() => sut.InvokeValidate()); } }