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());
}
}