docs: backfill XML documentation across 756 files
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped

Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public
members surfaced by commentchecker — resolves 5,847 of 5,869 issues
(99.6%) across three /fixdocs passes.
This commit is contained in:
Joseph Doherty
2026-05-28 08:10:17 -04:00
parent f9fc7dd2e1
commit 64e3fbe035
756 changed files with 9876 additions and 96 deletions
@@ -18,11 +18,20 @@ public sealed class AbCipCommandBaseTests
[CliFx.Attributes.Command("test")]
private sealed class TestableCommand : AbCipCommandBase
{
/// <summary>
/// Invokes the protected <see cref="AbCipCommandBase.BuildOptions"/> method.
/// </summary>
/// <param name="tags">The tags to pass to BuildOptions.</param>
/// <returns>The configured driver options.</returns>
public AbCipDriverOptions InvokeBuildOptions(IReadOnlyList<AbCipTagDefinition> tags)
=> BuildOptions(tags);
/// <summary>
/// Gets the protected <see cref="AbCipCommandBase.DriverInstanceId"/> property.
/// </summary>
public string InvokeDriverInstanceId => DriverInstanceId;
/// <inheritdoc />
public override ValueTask ExecuteAsync(CliFx.Infrastructure.IConsole console)
=> ValueTask.CompletedTask;
}
@@ -34,6 +43,9 @@ public sealed class AbCipCommandBaseTests
DataType: AbCipDataType.DInt,
Writable: false);
/// <summary>
/// Verifies that BuildOptions disables probe to prevent racing operator reads.
/// </summary>
[Fact]
public void BuildOptions_disables_probe_so_cli_does_not_race_operator_reads()
{
@@ -49,6 +61,9 @@ public sealed class AbCipCommandBaseTests
options.Probe.Enabled.ShouldBeFalse();
}
/// <summary>
/// Verifies that BuildOptions disables controller browse.
/// </summary>
[Fact]
public void BuildOptions_disables_controller_browse()
{
@@ -64,6 +79,9 @@ public sealed class AbCipCommandBaseTests
options.EnableControllerBrowse.ShouldBeFalse();
}
/// <summary>
/// Verifies that BuildOptions disables alarm projection.
/// </summary>
[Fact]
public void BuildOptions_disables_alarm_projection()
{
@@ -79,6 +97,9 @@ public sealed class AbCipCommandBaseTests
options.EnableAlarmProjection.ShouldBeFalse();
}
/// <summary>
/// Verifies that BuildOptions produces one device with gateway family and derived name.
/// </summary>
[Fact]
public void BuildOptions_produces_one_device_with_gateway_family_and_derived_name()
{
@@ -98,6 +119,9 @@ public sealed class AbCipCommandBaseTests
device.DeviceName.ShouldBe("cli-CompactLogix");
}
/// <summary>
/// Verifies that BuildOptions passes the supplied tag list verbatim.
/// </summary>
[Fact]
public void BuildOptions_passes_supplied_tag_list_verbatim()
{
@@ -116,6 +140,9 @@ public sealed class AbCipCommandBaseTests
options.Tags[1].Name.ShouldBe("t2");
}
/// <summary>
/// Verifies that BuildOptions carries TimeoutMs through to Timeout.
/// </summary>
[Fact]
public void BuildOptions_carries_TimeoutMs_through_to_Timeout()
{
@@ -131,6 +158,9 @@ public sealed class AbCipCommandBaseTests
options.Timeout.ShouldBe(TimeSpan.FromMilliseconds(7500));
}
/// <summary>
/// Verifies that DriverInstanceId embeds gateway for log disambiguation.
/// </summary>
[Fact]
public void DriverInstanceId_embeds_gateway_for_log_disambiguation()
{
@@ -143,6 +173,9 @@ public sealed class AbCipCommandBaseTests
cmd.InvokeDriverInstanceId.ShouldBe("abcip-cli-ab://10.0.0.5/1,0");
}
/// <summary>
/// Verifies that Timeout setter is inert and does not silently swallow assignments.
/// </summary>
[Fact]
public void Timeout_setter_is_inert_and_does_not_silently_swallow_assignments()
{
@@ -156,6 +189,10 @@ public sealed class AbCipCommandBaseTests
});
}
/// <summary>
/// Verifies that Timeout getter throws CommandException when TimeoutMs is non-positive.
/// </summary>
/// <param name="badMs">A non-positive timeout value to test.</param>
[Theory]
[InlineData(0)]
[InlineData(-1)]
@@ -173,6 +210,9 @@ public sealed class AbCipCommandBaseTests
ex.Message.ShouldContain("--timeout-ms");
}
/// <summary>
/// Verifies that RejectStructure throws for Structure DataType.
/// </summary>
[Fact]
public void RejectStructure_throws_for_Structure_DataType()
{
@@ -181,6 +221,10 @@ public sealed class AbCipCommandBaseTests
ex.Message.ShouldContain("Structure");
}
/// <summary>
/// Verifies that RejectStructure passes for atomic data types.
/// </summary>
/// <param name="type">The atomic data type to test.</param>
[Theory]
[InlineData(AbCipDataType.DInt)]
[InlineData(AbCipDataType.Bool)]
@@ -12,6 +12,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.Tests;
[Trait("Category", "Unit")]
public sealed class SubscribeCommandIntervalTests
{
/// <summary>Verifies that validate interval rejects non-positive milliseconds.</summary>
/// <param name="badMs">A non-positive interval value that should be rejected.</param>
[Theory]
[InlineData(0)]
[InlineData(-1)]
@@ -23,6 +25,8 @@ public sealed class SubscribeCommandIntervalTests
ex.Message.ShouldContain("--interval-ms");
}
/// <summary>Verifies that validate interval accepts positive milliseconds.</summary>
/// <param name="goodMs">A positive interval value that should be accepted.</param>
[Theory]
[InlineData(1)]
[InlineData(250)]
@@ -11,6 +11,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.Tests;
[Trait("Category", "Unit")]
public sealed class WriteCommandParseValueTests
{
/// <summary>Verifies ParseValue accepts common boolean aliases.</summary>
/// <param name="raw">Input string to parse.</param>
/// <param name="expected">Expected boolean value.</param>
[Theory]
[InlineData("true", true)]
[InlineData("0", false)]
@@ -21,6 +24,7 @@ public sealed class WriteCommandParseValueTests
WriteCommand.ParseValue(raw, AbCipDataType.Bool).ShouldBe(expected);
}
/// <summary>Verifies ParseValue rejects invalid boolean input.</summary>
[Fact]
public void ParseValue_Bool_rejects_garbage()
{
@@ -28,6 +32,7 @@ public sealed class WriteCommandParseValueTests
() => WriteCommand.ParseValue("maybe", AbCipDataType.Bool));
}
/// <summary>Verifies ParseValue correctly widens SInt to signed byte.</summary>
[Fact]
public void ParseValue_SInt_widens_to_sbyte()
{
@@ -35,12 +40,14 @@ public sealed class WriteCommandParseValueTests
WriteCommand.ParseValue("127", AbCipDataType.SInt).ShouldBe((sbyte)127);
}
/// <summary>Verifies ParseValue correctly parses signed 16-bit integers.</summary>
[Fact]
public void ParseValue_Int_signed_16bit()
{
WriteCommand.ParseValue("-32768", AbCipDataType.Int).ShouldBe((short)-32768);
}
/// <summary>Verifies ParseValue handles DInt and Dt data types as 32-bit integers.</summary>
[Fact]
public void ParseValue_DInt_and_Dt_both_land_on_int()
{
@@ -48,12 +55,14 @@ public sealed class WriteCommandParseValueTests
WriteCommand.ParseValue("1234567", AbCipDataType.Dt).ShouldBeOfType<int>();
}
/// <summary>Verifies ParseValue correctly parses 64-bit long integers.</summary>
[Fact]
public void ParseValue_LInt_64bit()
{
WriteCommand.ParseValue("9223372036854775807", AbCipDataType.LInt).ShouldBe(long.MaxValue);
}
/// <summary>Verifies ParseValue respects unsigned type bounds.</summary>
[Fact]
public void ParseValue_unsigned_range_respects_bounds()
{
@@ -62,24 +71,28 @@ public sealed class WriteCommandParseValueTests
WriteCommand.ParseValue("4294967295", AbCipDataType.UDInt).ShouldBeOfType<uint>();
}
/// <summary>Verifies ParseValue correctly parses floating-point with invariant culture.</summary>
[Fact]
public void ParseValue_Real_invariant_culture_decimal()
{
WriteCommand.ParseValue("3.14", AbCipDataType.Real).ShouldBe(3.14f);
}
/// <summary>Verifies ParseValue correctly handles double-precision floating-point.</summary>
[Fact]
public void ParseValue_LReal_handles_double_precision()
{
WriteCommand.ParseValue("2.718281828", AbCipDataType.LReal).ShouldBeOfType<double>();
}
/// <summary>Verifies ParseValue passes through string values unchanged.</summary>
[Fact]
public void ParseValue_String_passthrough()
{
WriteCommand.ParseValue("hello logix", AbCipDataType.String).ShouldBe("hello logix");
}
/// <summary>Verifies ParseValue throws for non-numeric input on numeric types.</summary>
[Fact]
public void ParseValue_non_numeric_for_numeric_types_throws_CommandException()
{
@@ -87,6 +100,7 @@ public sealed class WriteCommandParseValueTests
() => WriteCommand.ParseValue("xyz", AbCipDataType.DInt));
}
/// <summary>Verifies ParseValue throws for out-of-range numeric values.</summary>
[Fact]
public void ParseValue_out_of_range_throws_CommandException()
{
@@ -95,6 +109,9 @@ public sealed class WriteCommandParseValueTests
() => WriteCommand.ParseValue("999", AbCipDataType.SInt));
}
/// <summary>Verifies ParseValue exception messages include the input and data type for context.</summary>
/// <param name="raw">Input string to parse.</param>
/// <param name="type">Data type to parse into.</param>
[Theory]
[InlineData("12x", AbCipDataType.Int)]
[InlineData("3.14", AbCipDataType.DInt)]
@@ -108,6 +125,10 @@ public sealed class WriteCommandParseValueTests
ex.Message.ShouldContain(type.ToString());
}
/// <summary>Verifies SynthesiseTagName preserves the tag path verbatim in the output.</summary>
/// <param name="path">Tag path.</param>
/// <param name="type">Data type.</param>
/// <param name="expected">Expected synthesized tag name.</param>
[Theory]
[InlineData("Motor01_Speed", AbCipDataType.Real, "Motor01_Speed:Real")]
[InlineData("Program:Main.Counter", AbCipDataType.DInt, "Program:Main.Counter:DInt")]
@@ -27,9 +27,16 @@ public sealed class BuildOptionsTests
[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.");
}
@@ -50,6 +57,7 @@ public sealed class BuildOptionsTests
Writable: true),
];
/// <summary>Verifies that probe is disabled for CLI one-shot runs.</summary>
[Fact]
public void BuildOptions_disables_probe_for_cli_oneshot_runs()
{
@@ -67,6 +75,7 @@ public sealed class BuildOptionsTests
"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()
{
@@ -85,6 +94,7 @@ public sealed class BuildOptionsTests
options.Devices[0].DeviceName.ShouldBe("cli-MicroLogix");
}
/// <summary>Verifies that tag list is forwarded verbatim to the options.</summary>
[Fact]
public void BuildOptions_forwards_tag_list_verbatim()
{
@@ -100,6 +110,7 @@ public sealed class BuildOptionsTests
options.Tags.ShouldBe(SampleTags);
}
/// <summary>Verifies that timeout-ms option is propagated to the driver options.</summary>
[Fact]
public void BuildOptions_propagates_timeout_ms()
{
@@ -115,6 +126,7 @@ public sealed class BuildOptionsTests
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()
{
@@ -30,6 +30,7 @@ public sealed class CommandMetadataTests
// ---------- Driver.AbLegacy.Cli-006 — ProbeCommand --type needs short alias 't' ----------
/// <summary>Verifies that ProbeCommand --type has short alias -t.</summary>
[Fact]
public void ProbeCommand_type_has_short_alias_t()
{
@@ -38,6 +39,9 @@ public sealed class CommandMetadataTests
attr.ShortName.ShouldBe('t');
}
/// <summary>Verifies that other commands keep the --type short alias as -t.</summary>
/// <param name="commandType">The command type to inspect for the --type option.</param>
/// <param name="propName">The property name of the --type option on the command.</param>
[Theory]
[InlineData(typeof(ReadCommand), nameof(ReadCommand.DataType))]
[InlineData(typeof(WriteCommand), nameof(WriteCommand.DataType))]
@@ -53,6 +57,7 @@ public sealed class CommandMetadataTests
// ---------- Driver.AbLegacy.Cli-002 — WriteCommand --value help lists full bool alias set ----------
/// <summary>Verifies that WriteCommand --value help lists the full boolean alias set.</summary>
[Fact]
public void WriteCommand_value_help_lists_full_boolean_alias_set()
{
@@ -68,6 +73,7 @@ public sealed class CommandMetadataTests
// ---------- Driver.AbLegacy.Cli-005 — SubscribeCommand --interval-ms help notes 250ms floor ----------
/// <summary>Verifies that SubscribeCommand --interval-ms help notes the PollGroupEngine floor.</summary>
[Fact]
public void SubscribeCommand_interval_ms_help_notes_PollGroupEngine_floor()
{
@@ -11,6 +11,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Tests;
[Trait("Category", "Unit")]
public sealed class WriteCommandParseValueTests
{
/// <summary>Verifies that bit values accept common aliases like "true", "yes", "0", and "OFF".</summary>
/// <param name="raw">The raw string value to parse.</param>
/// <param name="expected">The expected boolean result.</param>
[Theory]
[InlineData("true", true)]
[InlineData("0", false)]
@@ -21,6 +24,7 @@ public sealed class WriteCommandParseValueTests
WriteCommand.ParseValue(raw, AbLegacyDataType.Bit).ShouldBe(expected);
}
/// <summary>Verifies that signed 16-bit integer values are parsed correctly within range.</summary>
[Fact]
public void ParseValue_Int_signed_16bit()
{
@@ -28,6 +32,7 @@ public sealed class WriteCommandParseValueTests
WriteCommand.ParseValue("32767", AbLegacyDataType.Int).ShouldBe((short)32767);
}
/// <summary>Verifies that AnalogInt values parse using the same semantics as Int.</summary>
[Fact]
public void ParseValue_AnalogInt_parses_same_as_Int()
{
@@ -35,6 +40,7 @@ public sealed class WriteCommandParseValueTests
WriteCommand.ParseValue("100", AbLegacyDataType.AnalogInt).ShouldBeOfType<short>();
}
/// <summary>Verifies that 32-bit integer values are parsed correctly within range.</summary>
[Fact]
public void ParseValue_Long_32bit()
{
@@ -42,18 +48,22 @@ public sealed class WriteCommandParseValueTests
WriteCommand.ParseValue("2147483647", AbLegacyDataType.Long).ShouldBe(int.MaxValue);
}
/// <summary>Verifies that float values are parsed using invariant culture.</summary>
[Fact]
public void ParseValue_Float_invariant_culture()
{
WriteCommand.ParseValue("3.14", AbLegacyDataType.Float).ShouldBe(3.14f);
}
/// <summary>Verifies that string values are returned unchanged.</summary>
[Fact]
public void ParseValue_String_passthrough()
{
WriteCommand.ParseValue("hello slc", AbLegacyDataType.String).ShouldBe("hello slc");
}
/// <summary>Verifies that Timer, Counter, and Control element types parse as 32-bit integers.</summary>
/// <param name="type">The AB Legacy data type to test.</param>
[Theory]
[InlineData(AbLegacyDataType.TimerElement)]
[InlineData(AbLegacyDataType.CounterElement)]
@@ -64,6 +74,7 @@ public sealed class WriteCommandParseValueTests
WriteCommand.ParseValue("42", type).ShouldBeOfType<int>();
}
/// <summary>Verifies that unknown string values are rejected for bit type.</summary>
[Fact]
public void ParseValue_Bit_rejects_unknown_strings()
{
@@ -71,6 +82,7 @@ public sealed class WriteCommandParseValueTests
() => WriteCommand.ParseValue("perhaps", AbLegacyDataType.Bit));
}
/// <summary>Verifies that non-numeric values throw CommandException for numeric types.</summary>
[Fact]
public void ParseValue_non_numeric_for_numeric_types_throws_CommandException()
{
@@ -79,6 +91,9 @@ public sealed class WriteCommandParseValueTests
() => WriteCommand.ParseValue("xyz", AbLegacyDataType.Int));
}
/// <summary>Verifies that out-of-range values throw CommandException.</summary>
/// <param name="raw">The raw string value that is out of range.</param>
/// <param name="type">The AB Legacy data type to test.</param>
[Theory]
[InlineData("99999", AbLegacyDataType.Int)] // short range is ±32767
[InlineData("-99999", AbLegacyDataType.Int)]
@@ -90,6 +105,10 @@ public sealed class WriteCommandParseValueTests
() => WriteCommand.ParseValue(raw, type));
}
/// <summary>Verifies that PCCC addresses are preserved verbatim in synthesized tag names.</summary>
/// <param name="address">The PCCC address string to test.</param>
/// <param name="type">The AB Legacy data type.</param>
/// <param name="expected">The expected synthesized tag name.</param>
[Theory]
[InlineData("N7:0", AbLegacyDataType.Int, "N7:0:Int")]
[InlineData("B3:0/3", AbLegacyDataType.Bit, "B3:0/3:Bit")]
@@ -12,6 +12,7 @@ public sealed class SnapshotFormatterTests
private static readonly DateTime FixedTime =
new(2026, 4, 21, 12, 34, 56, 789, DateTimeKind.Utc);
/// <summary>Verifies that Format includes tag value, status, and both timestamps.</summary>
[Fact]
public void Format_includes_tag_value_status_and_both_timestamps()
{
@@ -25,6 +26,9 @@ public sealed class SnapshotFormatterTests
output.ShouldContain("Server Time: 2026-04-21T12:34:56.789Z");
}
/// <summary>Verifies that FormatStatus names well-known status codes.</summary>
/// <param name="status">The OPC UA status code to format.</param>
/// <param name="expectedName">The expected name for the status code.</param>
[Theory]
// Numeric codes are the canonical OPC Foundation Opc.Ua.StatusCodes values.
[InlineData(0x00000000u, "Good")]
@@ -49,6 +53,9 @@ public sealed class SnapshotFormatterTests
SnapshotFormatter.FormatStatus(status).ShouldBe($"0x{status:X8} ({expectedName})");
}
/// <summary>Verifies that FormatStatus does not apply pre-fix wrong names.</summary>
/// <param name="status">The OPC UA status code to format.</param>
/// <param name="wrongName">The incorrect name that must not appear in the output.</param>
[Theory]
// Regression for Driver.Cli.Common-001: these codes were previously mapped to the
// wrong names. The hex values below are what the buggy shortlist used; they must
@@ -66,6 +73,7 @@ public sealed class SnapshotFormatterTests
SnapshotFormatter.FormatStatus(status).ShouldNotContain(wrongName);
}
/// <summary>Verifies that FormatStatus unknown codes fall back to severity class.</summary>
[Fact]
public void FormatStatus_unknown_codes_fall_back_to_severity_class()
{
@@ -74,6 +82,7 @@ public sealed class SnapshotFormatterTests
SnapshotFormatter.FormatStatus(0xDEADBEEFu).ShouldBe("0xDEADBEEF (Bad)");
}
/// <summary>Verifies that FormatValue renders null as a placeholder.</summary>
[Fact]
public void FormatValue_renders_null_as_placeholder()
{
@@ -83,6 +92,7 @@ public sealed class SnapshotFormatterTests
output.ShouldContain("Source Time: -"); // null timestamp → dash
}
/// <summary>Verifies that FormatValue formats booleans as lowercase.</summary>
[Fact]
public void FormatValue_formats_booleans_lowercase()
{
@@ -90,6 +100,7 @@ public sealed class SnapshotFormatterTests
SnapshotFormatter.Format("Coil", snap).ShouldContain("Value: true");
}
/// <summary>Verifies that FormatValue formats floats using invariant culture.</summary>
[Fact]
public void FormatValue_formats_floats_invariant_culture()
{
@@ -99,6 +110,7 @@ public sealed class SnapshotFormatterTests
SnapshotFormatter.Format("F8:0", snap).ShouldContain("3.14");
}
/// <summary>Verifies that FormatValue quotes strings.</summary>
[Fact]
public void FormatValue_quotes_strings()
{
@@ -106,6 +118,7 @@ public sealed class SnapshotFormatterTests
SnapshotFormatter.Format("Msg", snap).ShouldContain("\"hello\"");
}
/// <summary>Verifies that FormatWrite shows status with tag name.</summary>
[Fact]
public void FormatWrite_shows_status_with_tag_name()
{
@@ -114,6 +127,7 @@ public sealed class SnapshotFormatterTests
.ShouldBe("Write Scratch: 0x00000000 (Good)");
}
/// <summary>Verifies that FormatTable aligns columns and includes header separator.</summary>
[Fact]
public void FormatTable_aligns_columns_and_includes_header_separator()
{
@@ -134,6 +148,7 @@ public sealed class SnapshotFormatterTests
table.ShouldContain("0x00000000");
}
/// <summary>Verifies that FormatTable rejects mismatched lengths.</summary>
[Fact]
public void FormatTable_rejects_mismatched_lengths()
{
@@ -142,6 +157,7 @@ public sealed class SnapshotFormatterTests
new[] { new DataValueSnapshot(1, 0u, FixedTime, FixedTime) }));
}
/// <summary>Verifies that FormatTimestamp normalises local kind to UTC.</summary>
[Fact]
public void FormatTimestamp_normalises_local_kind_to_utc()
{
@@ -154,6 +170,9 @@ public sealed class SnapshotFormatterTests
// --- Driver.Cli.Common-002: sub-code bits in status codes ---
/// <summary>Verifies that FormatStatus with sub-code bits resolves to named class.</summary>
/// <param name="statusCode">The OPC UA status code with sub-code bits.</param>
/// <param name="expectedName">The expected class name.</param>
[Theory]
// Status codes with non-zero low-word flag bits must still resolve to the named
// high-word class (Driver.Cli.Common-002).
@@ -166,6 +185,9 @@ public sealed class SnapshotFormatterTests
SnapshotFormatter.FormatStatus(statusCode).ShouldContain($"({expectedName})");
}
/// <summary>Verifies that FormatStatus unknown sub-code falls back to severity class.</summary>
/// <param name="statusCode">The OPC UA status code with unknown sub-code.</param>
/// <param name="expectedSeverity">The expected severity class name.</param>
[Theory]
// Unknown sub-codes fall back to the severity class (Good / Uncertain / Bad).
[InlineData(0x80990000u, "Bad")] // Unknown bad sub-code → "Bad"
@@ -179,6 +201,7 @@ public sealed class SnapshotFormatterTests
// --- FormatTable empty-input ---
/// <summary>Verifies that FormatTable with empty input returns header only.</summary>
[Fact]
public void FormatTable_with_empty_input_returns_header_only()
{
@@ -205,15 +228,19 @@ public sealed class DriverCommandBaseTests
[CliFx.Attributes.Command("test-stub", Description = "Test stub — not a real command.")]
private sealed class TestCommand : DriverCommandBase
{
/// <inheritdoc />
public override TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(1);
/// <inheritdoc />
public override ValueTask ExecuteAsync(IConsole console) => default;
// Expose protected methods for testing.
/// <summary>Exposes ConfigureLogging for testing.</summary>
public void InvokeConfigureLogging() => ConfigureLogging();
/// <summary>Exposes FlushLogging for testing.</summary>
public static void InvokeFlushLogging() => FlushLogging();
}
/// <summary>Verifies that ConfigureLogging non-verbose sets warning level.</summary>
[Fact]
public void ConfigureLogging_non_verbose_sets_warning_level()
{
@@ -226,6 +253,7 @@ public sealed class DriverCommandBaseTests
DriverCommandBase_TestCommand_Teardown();
}
/// <summary>Verifies that ConfigureLogging verbose sets debug level.</summary>
[Fact]
public void ConfigureLogging_verbose_sets_debug_level()
{
@@ -236,6 +264,7 @@ public sealed class DriverCommandBaseTests
DriverCommandBase_TestCommand_Teardown();
}
/// <summary>Verifies that ConfigureLogging is idempotent and second call is noop.</summary>
[Fact]
public void ConfigureLogging_is_idempotent_second_call_is_noop()
{
@@ -252,6 +281,7 @@ public sealed class DriverCommandBaseTests
DriverCommandBase_TestCommand_Teardown();
}
/// <summary>Verifies that FlushLogging does not throw.</summary>
[Fact]
public void FlushLogging_does_not_throw()
{
@@ -16,6 +16,8 @@ public sealed class CommandDisposalConventionsTests
{
private static readonly string CommandsDir = LocateCommandsDir();
/// <summary>Verifies that a command does not explicitly call ShutdownAsync (relying on DisposeAsync instead).</summary>
/// <param name="commandFile">The command source file name to inspect.</param>
[Theory]
[InlineData("ProbeCommand.cs")]
[InlineData("ReadCommand.cs")]
@@ -33,6 +35,8 @@ public sealed class CommandDisposalConventionsTests
source.ShouldNotContain("driver.ShutdownAsync(");
}
/// <summary>Verifies that a command uses await using for FocasDriver disposal.</summary>
/// <param name="commandFile">The command source file name to inspect.</param>
[Theory]
[InlineData("ProbeCommand.cs")]
[InlineData("ReadCommand.cs")]
@@ -18,10 +18,15 @@ public sealed class FocasCommandBaseBuildOptionsTests
[Command("noop-test", Description = "Test-only probe of FocasCommandBase.BuildOptions.")]
private sealed class ProbeOnly : FocasCommandBase
{
/// <inheritdoc />
public override ValueTask ExecuteAsync(IConsole console) => default;
/// <summary>Invokes the BuildOptions method with the given tags.</summary>
/// <param name="tags">The list of tag definitions.</param>
/// <returns>The built driver options.</returns>
public FocasDriverOptions Invoke(IReadOnlyList<FocasTagDefinition> tags) => BuildOptions(tags);
}
/// <summary>Verifies that BuildOptions disables probe for one-shot CLI runs.</summary>
[Fact]
public void BuildOptions_disables_probe_for_one_shot_cli_runs()
{
@@ -39,6 +44,7 @@ public sealed class FocasCommandBaseBuildOptionsTests
options.Probe.Enabled.ShouldBeFalse();
}
/// <summary>Verifies that BuildOptions maps TimeoutMs to Timeout TimeSpan.</summary>
[Fact]
public void BuildOptions_maps_TimeoutMs_to_Timeout_TimeSpan()
{
@@ -49,6 +55,7 @@ public sealed class FocasCommandBaseBuildOptionsTests
options.Timeout.ShouldBe(TimeSpan.FromMilliseconds(7500));
}
/// <summary>Verifies that BuildOptions flows host, port, and series through.</summary>
[Fact]
public void BuildOptions_flows_host_port_series_through()
{
@@ -67,6 +74,7 @@ public sealed class FocasCommandBaseBuildOptionsTests
options.Devices[0].Series.ShouldBe(FocasCncSeries.Zero_i_F);
}
/// <summary>Verifies that BuildOptions forwards tag list verbatim.</summary>
[Fact]
public void BuildOptions_forwards_tag_list_verbatim()
{
@@ -22,14 +22,20 @@ public sealed class FocasCommandBaseValidationTests
[Command("noop-test", Description = "Test-only probe of FocasCommandBase.ValidateOptions.")]
private sealed class Probe : FocasCommandBase
{
/// <summary>Gets or sets the interval in milliseconds.</summary>
public int IntervalMs { get; init; }
/// <inheritdoc />
public override ValueTask ExecuteAsync(IConsole console) => default;
/// <summary>Invokes option validation with interval.</summary>
public void InvokeValidate() => ValidateOptions(IntervalMs);
/// <summary>Invokes option validation without interval.</summary>
public void InvokeValidateNoInterval() => ValidateOptions(intervalMs: null);
}
/// <summary>Verifies that default options are accepted.</summary>
[Fact]
public void Validate_accepts_default_options()
{
@@ -37,6 +43,8 @@ public sealed class FocasCommandBaseValidationTests
Should.NotThrow(() => sut.InvokeValidate());
}
/// <summary>Verifies that out-of-range CNC port is rejected.</summary>
/// <param name="port">The port value to test.</param>
[Theory]
[InlineData(0)]
[InlineData(-1)]
@@ -48,6 +56,8 @@ public sealed class FocasCommandBaseValidationTests
ex.Message.ShouldContain("cnc-port", Case.Insensitive);
}
/// <summary>Verifies that non-positive timeout is rejected.</summary>
/// <param name="timeoutMs">The timeout value in milliseconds to test.</param>
[Theory]
[InlineData(0)]
[InlineData(-100)]
@@ -58,6 +68,8 @@ public sealed class FocasCommandBaseValidationTests
ex.Message.ShouldContain("timeout-ms", Case.Insensitive);
}
/// <summary>Verifies that non-positive interval is rejected.</summary>
/// <param name="intervalMs">The interval value in milliseconds to test.</param>
[Theory]
[InlineData(0)]
[InlineData(-500)]
@@ -68,6 +80,7 @@ public sealed class FocasCommandBaseValidationTests
ex.Message.ShouldContain("interval-ms", Case.Insensitive);
}
/// <summary>Verifies that interval check is skipped when command omits it.</summary>
[Fact]
public void Validate_skips_interval_check_when_command_omits_it()
{
@@ -30,6 +30,7 @@ public sealed class SubscribeCommandConsoleHandlerTests
"Commands", "SubscribeCommand.cs"));
}
/// <summary>Verifies that SubscribeCommand documents why OnDataChange uses synchronous console output.</summary>
[Fact]
public void SubscribeCommand_explains_why_OnDataChange_uses_console_Output_synchronously()
{
@@ -41,6 +42,7 @@ public sealed class SubscribeCommandConsoleHandlerTests
source.ShouldContain("IConsole");
}
/// <summary>Verifies that SubscribeCommand serializes console writes with a lock.</summary>
[Fact]
public void SubscribeCommand_serialises_console_writes_with_a_lock()
{
@@ -14,6 +14,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli.Tests;
[Trait("Category", "Unit")]
public sealed class WriteCommandParseValueTests
{
/// <summary>Verifies that ParseValue accepts common boolean aliases for Bit type.</summary>
/// <param name="raw">The raw string input to parse.</param>
/// <param name="expected">The expected boolean result.</param>
[Theory]
[InlineData("true", true)]
[InlineData("0", false)]
@@ -24,6 +27,7 @@ public sealed class WriteCommandParseValueTests
WriteCommand.ParseValue(raw, FocasDataType.Bit).ShouldBe(expected);
}
/// <summary>Verifies that ParseValue rejects garbage Bit input as CommandException.</summary>
[Fact]
public void ParseValue_Bit_rejects_garbage_as_CommandException()
{
@@ -31,6 +35,7 @@ public sealed class WriteCommandParseValueTests
() => WriteCommand.ParseValue("maybe", FocasDataType.Bit));
}
/// <summary>Verifies that ParseValue accepts signed byte range.</summary>
[Fact]
public void ParseValue_Byte_signed_range()
{
@@ -39,6 +44,7 @@ public sealed class WriteCommandParseValueTests
WriteCommand.ParseValue("127", FocasDataType.Byte).ShouldBe((sbyte)127);
}
/// <summary>Verifies that ParseValue accepts signed 16-bit range.</summary>
[Fact]
public void ParseValue_Int16_signed_range()
{
@@ -46,24 +52,28 @@ public sealed class WriteCommandParseValueTests
WriteCommand.ParseValue("32767", FocasDataType.Int16).ShouldBe(short.MaxValue);
}
/// <summary>Verifies that ParseValue parses negative Int32 values.</summary>
[Fact]
public void ParseValue_Int32_parses_negative()
{
WriteCommand.ParseValue("-2147483648", FocasDataType.Int32).ShouldBe(int.MinValue);
}
/// <summary>Verifies that ParseValue parses Float32 in invariant culture.</summary>
[Fact]
public void ParseValue_Float32_invariant_culture()
{
WriteCommand.ParseValue("3.14", FocasDataType.Float32).ShouldBe(3.14f);
}
/// <summary>Verifies that ParseValue preserves Float64 higher precision.</summary>
[Fact]
public void ParseValue_Float64_higher_precision()
{
WriteCommand.ParseValue("2.718281828", FocasDataType.Float64).ShouldBeOfType<double>();
}
/// <summary>Verifies that ParseValue passes through string values unchanged.</summary>
[Fact]
public void ParseValue_String_passthrough()
{
@@ -74,6 +84,9 @@ public sealed class WriteCommandParseValueTests
// one-line CliFx error), NOT a raw FormatException stack trace. Previously the raw
// BCL parser exceptions leaked, contradicting how the Bit path already handled bad
// boolean input.
/// <summary>Verifies that ParseValue throws CommandException for non-numeric input to numeric types.</summary>
/// <param name="raw">The non-numeric raw input string.</param>
/// <param name="type">The FOCAS data type to attempt parsing into.</param>
[Theory]
[InlineData("xyz", FocasDataType.Byte)]
[InlineData("xyz", FocasDataType.Int16)]
@@ -88,6 +101,9 @@ public sealed class WriteCommandParseValueTests
}
// OverflowException from out-of-range input must also surface as CommandException.
/// <summary>Verifies that ParseValue throws CommandException for overflow in numeric types.</summary>
/// <param name="raw">The out-of-range raw input string.</param>
/// <param name="type">The FOCAS data type whose range is exceeded.</param>
[Theory]
[InlineData("128", FocasDataType.Byte)] // sbyte max + 1
[InlineData("-129", FocasDataType.Byte)] // sbyte min - 1
@@ -100,6 +116,7 @@ public sealed class WriteCommandParseValueTests
() => WriteCommand.ParseValue(raw, type));
}
/// <summary>Verifies that ParseValue CommandException message names the type and value.</summary>
[Fact]
public void ParseValue_CommandException_message_names_the_type_and_value()
{
@@ -109,6 +126,10 @@ public sealed class WriteCommandParseValueTests
ex.Message.ShouldContain("Int16");
}
/// <summary>Verifies that SynthesiseTagName preserves FOCAS address verbatim.</summary>
/// <param name="address">The FOCAS address string.</param>
/// <param name="type">The FOCAS data type appended to the tag name.</param>
/// <param name="expected">The expected synthesised tag name.</param>
[Theory]
[InlineData("R100", FocasDataType.Int16, "R100:Int16")]
[InlineData("X0.0", FocasDataType.Bit, "X0.0:Bit")]
@@ -18,6 +18,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.Tests;
[Trait("Category", "Unit")]
public sealed class CommandCancellationTests
{
/// <summary>Verifies that probe command gracefully handles cancellation during initialization.</summary>
[Fact]
public async Task ProbeCommand_swallows_cancellation_during_initialize()
{
@@ -29,6 +30,7 @@ public sealed class CommandCancellationTests
await Should.NotThrowAsync(async () => await sut.ExecuteAsync(console));
}
/// <summary>Verifies that read command gracefully handles cancellation during initialization.</summary>
[Fact]
public async Task ReadCommand_swallows_cancellation_during_initialize()
{
@@ -46,6 +48,7 @@ public sealed class CommandCancellationTests
await Should.NotThrowAsync(async () => await sut.ExecuteAsync(console));
}
/// <summary>Verifies that write command gracefully handles cancellation during initialization.</summary>
[Fact]
public async Task WriteCommand_swallows_cancellation_during_initialize()
{
@@ -23,11 +23,18 @@ public sealed class ModbusCommandBaseTests
[Command("noop-test", Description = "Test-only probe of ModbusCommandBase.BuildOptions.")]
private sealed class ProbeOnly : ModbusCommandBase
{
/// <inheritdoc />
public override ValueTask ExecuteAsync(IConsole console) => default;
/// <summary>Invokes BuildOptions with the given tags.</summary>
/// <param name="tags">The list of tag definitions to build options for.</param>
public ModbusDriverOptions Invoke(IReadOnlyList<ModbusTagDefinition> tags) => BuildOptions(tags);
/// <summary>Invokes ValidateEndpoint.</summary>
public void InvokeValidate() => ValidateEndpoint();
}
/// <summary>Verifies that BuildOptions disables probe for one-shot CLI runs.</summary>
[Fact]
public void BuildOptions_disables_probe_for_one_shot_cli_runs()
{
@@ -39,6 +46,7 @@ public sealed class ModbusCommandBaseTests
options.Probe.Enabled.ShouldBeFalse();
}
/// <summary>Verifies that BuildOptions maps TimeoutMs to Timeout TimeSpan.</summary>
[Fact]
public void BuildOptions_maps_TimeoutMs_to_Timeout_TimeSpan()
{
@@ -49,6 +57,7 @@ public sealed class ModbusCommandBaseTests
options.Timeout.ShouldBe(TimeSpan.FromMilliseconds(7500));
}
/// <summary>Verifies that BuildOptions AutoReconnect defaults to true when flag is unset.</summary>
[Fact]
public void BuildOptions_AutoReconnect_defaults_to_true_when_flag_unset()
{
@@ -59,6 +68,7 @@ public sealed class ModbusCommandBaseTests
options.AutoReconnect.ShouldBeTrue();
}
/// <summary>Verifies that BuildOptions AutoReconnect becomes false when disable reconnect flag is set.</summary>
[Fact]
public void BuildOptions_AutoReconnect_becomes_false_when_disable_reconnect_flag_set()
{
@@ -69,6 +79,7 @@ public sealed class ModbusCommandBaseTests
options.AutoReconnect.ShouldBeFalse();
}
/// <summary>Verifies that BuildOptions flows host, port, and unit through correctly.</summary>
[Fact]
public void BuildOptions_flows_host_port_unit_through()
{
@@ -81,6 +92,7 @@ public sealed class ModbusCommandBaseTests
options.UnitId.ShouldBe((byte)17);
}
/// <summary>Verifies that BuildOptions forwards the tag list verbatim.</summary>
[Fact]
public void BuildOptions_forwards_tag_list_verbatim()
{
@@ -96,6 +108,8 @@ public sealed class ModbusCommandBaseTests
// --- Driver.Modbus.Cli-003: parse-time endpoint validation -------------------------------
/// <summary>Verifies that ValidateEndpoint rejects ports outside the range 1 to 65535.</summary>
/// <param name="port">The port value that should be rejected.</param>
[Theory]
[InlineData(0)]
[InlineData(-1)]
@@ -109,6 +123,8 @@ public sealed class ModbusCommandBaseTests
Should.Throw<CliFx.Exceptions.CommandException>(() => sut.InvokeValidate());
}
/// <summary>Verifies that ValidateEndpoint accepts ports in the valid range.</summary>
/// <param name="port">The port value that should be accepted.</param>
[Theory]
[InlineData(1)]
[InlineData(502)]
@@ -120,6 +136,8 @@ public sealed class ModbusCommandBaseTests
Should.NotThrow(() => sut.InvokeValidate());
}
/// <summary>Verifies that ValidateEndpoint rejects non-positive timeout values.</summary>
/// <param name="timeoutMs">The timeout value in milliseconds that should be rejected.</param>
[Theory]
[InlineData(0)]
[InlineData(-1)]
@@ -131,6 +149,8 @@ public sealed class ModbusCommandBaseTests
Should.Throw<CliFx.Exceptions.CommandException>(() => sut.InvokeValidate());
}
/// <summary>Verifies that ValidateEndpoint rejects unit IDs outside the range 1 to 247.</summary>
/// <param name="unitId">The unit ID value that should be rejected.</param>
[Theory]
[InlineData(0)] // broadcast — disallowed for unicast read/write requests
[InlineData(248)]
@@ -142,6 +162,8 @@ public sealed class ModbusCommandBaseTests
Should.Throw<CliFx.Exceptions.CommandException>(() => sut.InvokeValidate());
}
/// <summary>Verifies that ValidateEndpoint accepts unit IDs in the valid range.</summary>
/// <param name="unitId">The unit ID value that should be accepted.</param>
[Theory]
[InlineData(1)]
[InlineData(247)]
@@ -153,6 +175,7 @@ public sealed class ModbusCommandBaseTests
Should.NotThrow(() => sut.InvokeValidate());
}
/// <summary>Verifies that ValidateEndpoint accepts default options.</summary>
[Fact]
public void ValidateEndpoint_accepts_default_options()
{
@@ -15,6 +15,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.Tests;
[Trait("Category", "Unit")]
public sealed class ProbeCommandTests
{
/// <summary>Verifies that ComputeVerdict returns OK when driver state is Healthy and status is Good.</summary>
[Fact]
public void ComputeVerdict_returns_OK_when_state_is_Healthy_and_status_is_Good()
{
@@ -23,6 +24,8 @@ public sealed class ProbeCommandTests
verdict.ShouldContain("OK");
}
/// <summary>Verifies that ComputeVerdict returns FAIL when driver state is not Healthy.</summary>
/// <param name="state">The driver state to test.</param>
[Theory]
[InlineData(DriverState.Faulted)]
[InlineData(DriverState.Reconnecting)]
@@ -34,6 +37,8 @@ public sealed class ProbeCommandTests
verdict.ShouldContain("FAIL");
}
/// <summary>Verifies that ComputeVerdict returns FAIL when snapshot status is Bad even if driver is Healthy.</summary>
/// <param name="statusCode">The OPC UA status code to test.</param>
[Theory]
[InlineData(0x80050000u)] // BadCommunicationError
[InlineData(0x800A0000u)] // BadTimeout
@@ -50,6 +55,7 @@ public sealed class ProbeCommandTests
verdict.ShouldContain("FAIL");
}
/// <summary>Verifies that ComputeVerdict returns DEGRADED for uncertain status with healthy driver.</summary>
[Fact]
public void ComputeVerdict_returns_DEGRADED_for_uncertain_status_with_healthy_driver()
{
@@ -7,6 +7,11 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.Tests;
[Trait("Category", "Unit")]
public sealed class ReadCommandTests
{
/// <summary>Verifies that SynthesiseTagName produces a stable, readable tag name format.</summary>
/// <param name="region">The Modbus region.</param>
/// <param name="address">The register address.</param>
/// <param name="type">The Modbus data type.</param>
/// <param name="expected">The expected synthesised tag name string.</param>
[Theory]
[InlineData(ModbusRegion.HoldingRegisters, 100, ModbusDataType.UInt16, "HR[100]:UInt16")]
[InlineData(ModbusRegion.Coils, 0, ModbusDataType.Bool, "Coil[0]:Bool")]
@@ -13,6 +13,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.Tests;
[Trait("Category", "Unit")]
public sealed class WriteCommandParseValueTests
{
/// <summary>Verifies that boolean parsing accepts multiple aliases for true and false.</summary>
/// <param name="raw">The raw string value to parse.</param>
/// <param name="expected">The expected boolean result.</param>
[Theory]
[InlineData("true", true)]
[InlineData("false", false)]
@@ -27,6 +30,7 @@ public sealed class WriteCommandParseValueTests
WriteCommand.ParseValue(raw, ModbusDataType.Bool).ShouldBe(expected);
}
/// <summary>Verifies that boolean parsing rejects unrecognized string values.</summary>
[Fact]
public void ParseValue_Bool_rejects_unknown_strings()
{
@@ -34,6 +38,7 @@ public sealed class WriteCommandParseValueTests
() => WriteCommand.ParseValue("maybe", ModbusDataType.Bool));
}
/// <summary>Verifies that Int16 parsing handles positive and negative values correctly.</summary>
[Fact]
public void ParseValue_Int16_parses_positive_and_negative()
{
@@ -41,6 +46,7 @@ public sealed class WriteCommandParseValueTests
WriteCommand.ParseValue("32767", ModbusDataType.Int16).ShouldBe((short)32767);
}
/// <summary>Verifies that both UInt16 and Bcd16 parsing return ushort type values.</summary>
[Fact]
public void ParseValue_UInt16_and_Bcd16_both_yield_ushort()
{
@@ -48,12 +54,14 @@ public sealed class WriteCommandParseValueTests
WriteCommand.ParseValue("65535", ModbusDataType.Bcd16).ShouldBeOfType<ushort>();
}
/// <summary>Verifies that Float32 parsing uses invariant culture with period as decimal separator.</summary>
[Fact]
public void ParseValue_Float32_uses_invariant_culture_period_as_decimal_separator()
{
WriteCommand.ParseValue("3.14", ModbusDataType.Float32).ShouldBe(3.14f);
}
/// <summary>Verifies that Float64 parsing maintains precision for larger decimal values.</summary>
[Fact]
public void ParseValue_Float64_handles_larger_precision()
{
@@ -62,12 +70,14 @@ public sealed class WriteCommandParseValueTests
((double)result).ShouldBe(2.718281828d, 0.0000001d);
}
/// <summary>Verifies that String parsing returns the raw input without modification.</summary>
[Fact]
public void ParseValue_String_returns_raw_string_unmodified()
{
WriteCommand.ParseValue("hello world", ModbusDataType.String).ShouldBe("hello world");
}
/// <summary>Verifies that BitInRegister parsing accepts boolean aliases.</summary>
[Fact]
public void ParseValue_BitInRegister_accepts_bool_aliases()
{
@@ -75,12 +85,14 @@ public sealed class WriteCommandParseValueTests
WriteCommand.ParseValue("0", ModbusDataType.BitInRegister).ShouldBe(false);
}
/// <summary>Verifies that Int32 parsing handles negative maximum values correctly.</summary>
[Fact]
public void ParseValue_Int32_parses_negative_max()
{
WriteCommand.ParseValue("-2147483648", ModbusDataType.Int32).ShouldBe(int.MinValue);
}
/// <summary>Verifies that parsing rejects non-numeric strings for numeric data types.</summary>
[Fact]
public void ParseValue_rejects_non_numeric_for_numeric_types()
{
@@ -17,6 +17,10 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.Tests;
[Trait("Category", "Unit")]
public sealed class WriteCommandRegionValidationTests
{
/// <summary>Verifies that write to read-only regions is rejected before reaching the driver.</summary>
/// <param name="region">The read-only Modbus region to attempt a write against.</param>
/// <param name="type">The data type used in the write attempt.</param>
/// <param name="value">The raw string value supplied to the write command.</param>
[Theory]
[InlineData(ModbusRegion.DiscreteInputs, ModbusDataType.Bool, "0")]
[InlineData(ModbusRegion.InputRegisters, ModbusDataType.UInt16, "1")]
@@ -38,6 +42,8 @@ public sealed class WriteCommandRegionValidationTests
async () => await sut.ExecuteAsync(console));
}
/// <summary>Verifies that Coils region requires Bool data type (Driver.Modbus.Cli-002).</summary>
/// <param name="type">The non-Bool data type that should be rejected for the Coils region.</param>
[Theory]
[InlineData(ModbusDataType.UInt16)]
[InlineData(ModbusDataType.Int16)]
@@ -16,6 +16,8 @@ public sealed class CommandDisposalConventionsTests
{
private static readonly string CommandsDir = LocateCommandsDir();
/// <summary>Verifies that commands do not call ShutdownAsync explicitly.</summary>
/// <param name="commandFile">The source file name of the command to inspect.</param>
[Theory]
[InlineData("ProbeCommand.cs")]
[InlineData("ReadCommand.cs")]
@@ -33,6 +35,8 @@ public sealed class CommandDisposalConventionsTests
source.ShouldNotContain("driver.ShutdownAsync(");
}
/// <summary>Verifies that commands use await using for S7Driver disposal.</summary>
/// <param name="commandFile">The source file name of the command to inspect.</param>
[Theory]
[InlineData("ProbeCommand.cs")]
[InlineData("ReadCommand.cs")]
@@ -20,13 +20,20 @@ public sealed class S7CommandBaseBuildOptionsTests
// helper. 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.
/// <summary>Test-only S7CommandBase concrete subclass that exposes the protected BuildOptions helper.</summary>
[Command("noop-test", Description = "Test-only probe of S7CommandBase.BuildOptions.")]
private sealed class ProbeOnly : S7CommandBase
{
/// <inheritdoc />
public override ValueTask ExecuteAsync(IConsole console) => default;
/// <summary>Invokes the BuildOptions method with the given tags.</summary>
/// <param name="tags">The tag definitions to pass to BuildOptions.</param>
/// <returns>The resulting S7DriverOptions.</returns>
public S7DriverOptions Invoke(IReadOnlyList<S7TagDefinition> tags) => BuildOptions(tags);
}
/// <summary>Verifies that BuildOptions disables probe for one-shot CLI runs.</summary>
[Fact]
public void BuildOptions_disables_probe_for_one_shot_cli_runs()
{
@@ -46,6 +53,7 @@ public sealed class S7CommandBaseBuildOptionsTests
options.Probe.Enabled.ShouldBeFalse();
}
/// <summary>Verifies that BuildOptions maps TimeoutMs to Timeout TimeSpan.</summary>
[Fact]
public void BuildOptions_maps_TimeoutMs_to_Timeout_TimeSpan()
{
@@ -56,6 +64,7 @@ public sealed class S7CommandBaseBuildOptionsTests
options.Timeout.ShouldBe(TimeSpan.FromMilliseconds(7500));
}
/// <summary>Verifies that BuildOptions flows host, port, cpu, rack, and slot through.</summary>
[Fact]
public void BuildOptions_flows_host_port_cpu_rack_slot_through()
{
@@ -78,6 +87,7 @@ public sealed class S7CommandBaseBuildOptionsTests
options.Slot.ShouldBe((short)2);
}
/// <summary>Verifies that BuildOptions forwards the tag list verbatim.</summary>
[Fact]
public void BuildOptions_forwards_tag_list_verbatim()
{
@@ -13,6 +13,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests;
[Trait("Category", "Unit")]
public sealed class SubscribeCommandConsoleHandlerCommentTests
{
/// <summary>Verifies that SubscribeCommand explains why OnDataChange uses console.Output synchronously.</summary>
[Fact]
public void SubscribeCommand_explains_why_OnDataChange_uses_console_Output_synchronously()
{
@@ -10,6 +10,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests;
[Trait("Category", "Unit")]
public sealed class WriteCommandParseValueTests
{
/// <summary>Verifies ParseValue accepts common boolean aliases.</summary>
/// <param name="raw">The raw string value to parse.</param>
/// <param name="expected">The expected boolean result.</param>
[Theory]
[InlineData("true", true)]
[InlineData("0", false)]
@@ -20,6 +23,7 @@ public sealed class WriteCommandParseValueTests
WriteCommand.ParseValue(raw, S7DataType.Bool).ShouldBe(expected);
}
/// <summary>Verifies ParseValue rejects invalid boolean values.</summary>
[Fact]
public void ParseValue_Bool_rejects_garbage()
{
@@ -27,6 +31,7 @@ public sealed class WriteCommandParseValueTests
() => WriteCommand.ParseValue("maybe", S7DataType.Bool));
}
/// <summary>Verifies ParseValue handles the full byte range.</summary>
[Fact]
public void ParseValue_Byte_ranges()
{
@@ -34,60 +39,70 @@ public sealed class WriteCommandParseValueTests
WriteCommand.ParseValue("255", S7DataType.Byte).ShouldBe((byte)255);
}
/// <summary>Verifies ParseValue handles signed 16-bit integers.</summary>
[Fact]
public void ParseValue_Int16_signed_range()
{
WriteCommand.ParseValue("-32768", S7DataType.Int16).ShouldBe((short)-32768);
}
/// <summary>Verifies ParseValue handles unsigned 16-bit integer maximum.</summary>
[Fact]
public void ParseValue_UInt16_unsigned_max()
{
WriteCommand.ParseValue("65535", S7DataType.UInt16).ShouldBe((ushort)65535);
}
/// <summary>Verifies ParseValue parses negative 32-bit integers.</summary>
[Fact]
public void ParseValue_Int32_parses_negative()
{
WriteCommand.ParseValue("-2147483648", S7DataType.Int32).ShouldBe(int.MinValue);
}
/// <summary>Verifies ParseValue parses unsigned 32-bit integer maximum.</summary>
[Fact]
public void ParseValue_UInt32_parses_max()
{
WriteCommand.ParseValue("4294967295", S7DataType.UInt32).ShouldBe(uint.MaxValue);
}
/// <summary>Verifies ParseValue parses signed 64-bit integer minimum.</summary>
[Fact]
public void ParseValue_Int64_parses_min()
{
WriteCommand.ParseValue("-9223372036854775808", S7DataType.Int64).ShouldBe(long.MinValue);
}
/// <summary>Verifies ParseValue parses unsigned 64-bit integer maximum.</summary>
[Fact]
public void ParseValue_UInt64_parses_max()
{
WriteCommand.ParseValue("18446744073709551615", S7DataType.UInt64).ShouldBe(ulong.MaxValue);
}
/// <summary>Verifies ParseValue parses 32-bit floats using invariant culture.</summary>
[Fact]
public void ParseValue_Float32_invariant_culture()
{
WriteCommand.ParseValue("3.14", S7DataType.Float32).ShouldBe(3.14f);
}
/// <summary>Verifies ParseValue parses 64-bit floats with higher precision.</summary>
[Fact]
public void ParseValue_Float64_higher_precision()
{
WriteCommand.ParseValue("2.718281828", S7DataType.Float64).ShouldBeOfType<double>();
}
/// <summary>Verifies ParseValue passes through strings unchanged.</summary>
[Fact]
public void ParseValue_String_passthrough()
{
WriteCommand.ParseValue("hallo siemens", S7DataType.String).ShouldBe("hallo siemens");
}
/// <summary>Verifies ParseValue parses DateTime in roundtrip format.</summary>
[Fact]
public void ParseValue_DateTime_parses_roundtrip_form()
{
@@ -96,6 +111,7 @@ public sealed class WriteCommandParseValueTests
((DateTime)result).Year.ShouldBe(2026);
}
/// <summary>Verifies ParseValue rejects non-numeric input for numeric types.</summary>
[Fact]
public void ParseValue_non_numeric_for_numeric_types_throws()
{
@@ -105,6 +121,7 @@ public sealed class WriteCommandParseValueTests
() => WriteCommand.ParseValue("xyz", S7DataType.Int16));
}
/// <summary>Verifies ParseValue throws CommandException on numeric overflow.</summary>
[Fact]
public void ParseValue_overflow_for_numeric_types_throws_CommandException()
{
@@ -113,6 +130,10 @@ public sealed class WriteCommandParseValueTests
() => WriteCommand.ParseValue("256", S7DataType.Byte));
}
/// <summary>Verifies SynthesiseTagName preserves S7 address verbatim.</summary>
/// <param name="address">The S7 address string to test.</param>
/// <param name="type">The S7 data type.</param>
/// <param name="expected">The expected synthesized tag name.</param>
[Theory]
[InlineData("DB1.DBW0", S7DataType.Int16, "DB1.DBW0:Int16")]
[InlineData("M0.0", S7DataType.Bool, "M0.0:Bool")]
@@ -22,6 +22,7 @@ public sealed class BrowseCommandFilterTests
SecurityClass: cls,
IsHistorized: false);
/// <summary>Verifies that the collector records variables in call order.</summary>
[Fact]
public void Collector_records_each_variable_in_call_order()
{
@@ -34,6 +35,7 @@ public sealed class BrowseCommandFilterTests
builder.Variables[1].BrowseName.ShouldBe("GVL.B");
}
/// <summary>Verifies that folder returns the same builder for flattening.</summary>
[Fact]
public void Folder_returns_same_builder_so_nested_variables_land_in_one_flat_list()
{
@@ -47,6 +49,7 @@ public sealed class BrowseCommandFilterTests
builder.Variables[0].BrowseName.ShouldBe("GVL.X");
}
/// <summary>Verifies that empty prefix returns all symbols up to max.</summary>
[Fact]
public void FilterAndLimit_empty_prefix_returns_everything_up_to_max()
{
@@ -61,6 +64,7 @@ public sealed class BrowseCommandFilterTests
matched.Count.ShouldBe(3);
}
/// <summary>Verifies that prefix filtering is case-sensitive.</summary>
[Fact]
public void FilterAndLimit_prefix_is_case_sensitive()
{
@@ -77,6 +81,7 @@ public sealed class BrowseCommandFilterTests
matched[0].BrowseName.ShouldBe("GVL_Fixture.x");
}
/// <summary>Verifies that zero max limit means unbounded.</summary>
[Fact]
public void FilterAndLimit_zero_max_means_unbounded()
{
@@ -88,24 +93,29 @@ public sealed class BrowseCommandFilterTests
limit.ShouldBe(10);
}
/// <summary>Verifies that output is capped to max when more symbols match.</summary>
[Fact]
public void FilterAndLimit_caps_to_max_when_more_matched()
{
BrowseCommand.PrintLimit(matchedCount: 1000, max: 50).ShouldBe(50);
}
/// <summary>Verifies that output is not padded when fewer symbols match.</summary>
[Fact]
public void FilterAndLimit_does_not_pad_to_max_when_fewer_matched()
{
BrowseCommand.PrintLimit(matchedCount: 3, max: 50).ShouldBe(3);
}
/// <summary>Verifies that ViewOnly attribute returns RO access tag.</summary>
[Fact]
public void AccessTag_returns_RO_for_ViewOnly_attribute()
{
BrowseCommand.AccessTag(Info(SecurityClassification.ViewOnly)).ShouldBe("RO");
}
/// <summary>Verifies that all classifications except ViewOnly return RW access tag.</summary>
/// <param name="cls">The security classification to test.</param>
[Theory]
[InlineData(SecurityClassification.FreeAccess)]
[InlineData(SecurityClassification.Operate)]
@@ -16,6 +16,8 @@ public sealed class SubscribeCommandMechanismTests
{
private sealed record StubHandle(string DiagnosticId) : ISubscriptionHandle;
/// <summary>Verifies that DescribeMechanism returns ADS notification for native handle.</summary>
/// <param name="diagId">The diagnostic ID of the subscription handle to classify.</param>
[Theory]
[InlineData("twincat-native-sub-1")]
[InlineData("twincat-native-sub-42")]
@@ -25,6 +27,8 @@ public sealed class SubscribeCommandMechanismTests
SubscribeCommand.DescribeMechanism(new StubHandle(diagId)).ShouldBe("ADS notification");
}
/// <summary>Verifies that DescribeMechanism returns polling for anything else.</summary>
/// <param name="diagId">The diagnostic ID of the subscription handle to classify.</param>
[Theory]
[InlineData("pollgroup-1")]
[InlineData("modbus-poll-7")]
@@ -15,6 +15,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Tests;
[Trait("Category", "Unit")]
public sealed class TwinCATCommandBaseTests
{
/// <summary>Verifies that the gateway string uses canonical ADS scheme with port.</summary>
[Fact]
public void Gateway_uses_canonical_ads_scheme_with_port()
{
@@ -27,6 +28,7 @@ public sealed class TwinCATCommandBaseTests
cmd.GatewayForTest.ShouldBe("ads://192.168.1.40.1.1:851");
}
/// <summary>Verifies that the gateway string round-trips through TwinCATAmsAddress.TryParse.</summary>
[Fact]
public void Gateway_round_trips_through_TwinCATAmsAddress_TryParse()
{
@@ -45,6 +47,7 @@ public sealed class TwinCATCommandBaseTests
parsed.Port.ShouldBe(852);
}
/// <summary>Verifies that the driver instance ID includes the AMS target.</summary>
[Fact]
public void DriverInstanceId_includes_ams_target()
{
@@ -57,6 +60,7 @@ public sealed class TwinCATCommandBaseTests
cmd.DriverInstanceIdForTest.ShouldBe("twincat-cli-127.0.0.1.1.1:851");
}
/// <summary>Verifies that timeout is a projection of TimeoutMs and initialization is a no-op.</summary>
[Fact]
public void Timeout_is_projection_of_TimeoutMs_and_init_is_noop()
{
@@ -69,6 +73,7 @@ public sealed class TwinCATCommandBaseTests
cmd.Timeout.ShouldBe(TimeSpan.FromMilliseconds(7777));
}
/// <summary>Verifies that BuildOptions wires device tags, timeout, and disables probe.</summary>
[Fact]
public void BuildOptions_wires_device_tags_timeout_and_disables_probe()
{
@@ -102,6 +107,7 @@ public sealed class TwinCATCommandBaseTests
options.UseNativeNotifications.ShouldBeTrue();
}
/// <summary>Verifies that the PollOnly flag flips UseNativeNotifications off.</summary>
[Fact]
public void BuildOptions_PollOnly_flips_UseNativeNotifications_off()
{
@@ -116,6 +122,7 @@ public sealed class TwinCATCommandBaseTests
// ---- Driver.TwinCAT.Cli-001 (range validation) ----
/// <summary>Verifies that validation rejects zero timeout.</summary>
[Fact]
public void Validate_rejects_zero_timeout()
{
@@ -129,6 +136,7 @@ public sealed class TwinCATCommandBaseTests
ex.Message.ShouldContain("--timeout-ms");
}
/// <summary>Verifies that validation rejects negative timeout.</summary>
[Fact]
public void Validate_rejects_negative_timeout()
{
@@ -141,6 +149,8 @@ public sealed class TwinCATCommandBaseTests
Should.Throw<CliFx.Exceptions.CommandException>(() => cmd.ValidateForTest());
}
/// <summary>Verifies that validation rejects out-of-range AMS port values.</summary>
/// <param name="port">The out-of-range AMS port value to test.</param>
[Theory]
[InlineData(0)]
[InlineData(-1)]
@@ -158,6 +168,8 @@ public sealed class TwinCATCommandBaseTests
ex.Message.ShouldContain("--ams-port");
}
/// <summary>Verifies that validation accepts in-range AMS port values.</summary>
/// <param name="port">The valid AMS port value to test.</param>
[Theory]
[InlineData(1)]
[InlineData(801)]
@@ -174,6 +186,7 @@ public sealed class TwinCATCommandBaseTests
Should.NotThrow(() => cmd.ValidateForTest());
}
/// <summary>Verifies that SubscribeCommand validation rejects zero interval.</summary>
[Fact]
public void SubscribeCommand_validate_rejects_zero_interval()
{
@@ -187,6 +200,7 @@ public sealed class TwinCATCommandBaseTests
ex.Message.ShouldContain("--interval-ms");
}
/// <summary>Verifies that SubscribeCommand validation rejects negative interval.</summary>
[Fact]
public void SubscribeCommand_validate_rejects_negative_interval()
{
@@ -201,6 +215,7 @@ public sealed class TwinCATCommandBaseTests
// ---- Driver.TwinCAT.Cli-004 (PollOnly off BrowseCommand surface) ----
/// <summary>Verifies that BrowseCommand does not expose the poll-only flag.</summary>
[Fact]
public void BrowseCommand_does_not_expose_poll_only_flag()
{
@@ -212,6 +227,7 @@ public sealed class TwinCATCommandBaseTests
props.ShouldNotContain(p => p.Name == "PollOnly");
}
/// <summary>Verifies that ProbeCommand still exposes the poll-only flag.</summary>
[Fact]
public void ProbeCommand_still_exposes_poll_only_flag()
{
@@ -224,6 +240,7 @@ public sealed class TwinCATCommandBaseTests
// ---- Driver.TwinCAT.Cli-005 (probe --type short alias) ----
/// <summary>Verifies that ProbeCommand type option carries the short alias 't'.</summary>
[Fact]
public void ProbeCommand_type_option_carries_short_alias_t()
{
@@ -12,6 +12,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Tests;
[Trait("Category", "Unit")]
public sealed class WriteCommandParseValueTests
{
/// <summary>Verifies that ParseValue Bool accepts common aliases.</summary>
/// <param name="raw">The raw input string to parse.</param>
/// <param name="expected">The expected boolean result.</param>
[Theory]
[InlineData("true", true)]
[InlineData("0", false)]
@@ -22,6 +25,7 @@ public sealed class WriteCommandParseValueTests
WriteCommand.ParseValue(raw, TwinCATDataType.Bool).ShouldBe(expected);
}
/// <summary>Verifies that ParseValue Bool rejects garbage.</summary>
[Fact]
public void ParseValue_Bool_rejects_garbage()
{
@@ -29,72 +33,84 @@ public sealed class WriteCommandParseValueTests
() => WriteCommand.ParseValue("maybe", TwinCATDataType.Bool));
}
/// <summary>Verifies that ParseValue SInt parses signed byte.</summary>
[Fact]
public void ParseValue_SInt_signed_byte()
{
WriteCommand.ParseValue("-128", TwinCATDataType.SInt).ShouldBe((sbyte)-128);
}
/// <summary>Verifies that ParseValue USInt parses unsigned byte.</summary>
[Fact]
public void ParseValue_USInt_unsigned_byte()
{
WriteCommand.ParseValue("255", TwinCATDataType.USInt).ShouldBe((byte)255);
}
/// <summary>Verifies that ParseValue Int parses signed 16-bit value.</summary>
[Fact]
public void ParseValue_Int_signed_16bit()
{
WriteCommand.ParseValue("-32768", TwinCATDataType.Int).ShouldBe((short)-32768);
}
/// <summary>Verifies that ParseValue UInt parses unsigned 16-bit value.</summary>
[Fact]
public void ParseValue_UInt_unsigned_16bit()
{
WriteCommand.ParseValue("65535", TwinCATDataType.UInt).ShouldBe((ushort)65535);
}
/// <summary>Verifies that ParseValue DInt parses int32 bounds.</summary>
[Fact]
public void ParseValue_DInt_int32_bounds()
{
WriteCommand.ParseValue("-2147483648", TwinCATDataType.DInt).ShouldBe(int.MinValue);
}
/// <summary>Verifies that ParseValue UDInt parses uint32 maximum.</summary>
[Fact]
public void ParseValue_UDInt_uint32_max()
{
WriteCommand.ParseValue("4294967295", TwinCATDataType.UDInt).ShouldBe(uint.MaxValue);
}
/// <summary>Verifies that ParseValue LInt parses int64 minimum.</summary>
[Fact]
public void ParseValue_LInt_int64_min()
{
WriteCommand.ParseValue("-9223372036854775808", TwinCATDataType.LInt).ShouldBe(long.MinValue);
}
/// <summary>Verifies that ParseValue ULInt parses uint64 maximum.</summary>
[Fact]
public void ParseValue_ULInt_uint64_max()
{
WriteCommand.ParseValue("18446744073709551615", TwinCATDataType.ULInt).ShouldBe(ulong.MaxValue);
}
/// <summary>Verifies that ParseValue Real uses invariant culture.</summary>
[Fact]
public void ParseValue_Real_invariant_culture()
{
WriteCommand.ParseValue("3.14", TwinCATDataType.Real).ShouldBe(3.14f);
}
/// <summary>Verifies that ParseValue LReal has higher precision.</summary>
[Fact]
public void ParseValue_LReal_higher_precision()
{
WriteCommand.ParseValue("2.718281828", TwinCATDataType.LReal).ShouldBeOfType<double>();
}
/// <summary>Verifies that ParseValue String passes through input unchanged.</summary>
[Fact]
public void ParseValue_String_passthrough()
{
WriteCommand.ParseValue("hallo beckhoff", TwinCATDataType.String).ShouldBe("hallo beckhoff");
}
/// <summary>Verifies that ParseValue WString passes through input unchanged.</summary>
[Fact]
public void ParseValue_WString_passthrough()
{
@@ -103,6 +119,8 @@ public sealed class WriteCommandParseValueTests
WriteCommand.ParseValue("überstall", TwinCATDataType.WString).ShouldBe("überstall");
}
/// <summary>Verifies that ParseValue IEC date/time variants land on uint32.</summary>
/// <param name="type">The IEC 61131-3 data type to parse.</param>
[Theory]
[InlineData(TwinCATDataType.Time)]
[InlineData(TwinCATDataType.Date)]
@@ -115,6 +133,7 @@ public sealed class WriteCommandParseValueTests
WriteCommand.ParseValue("1234567", type).ShouldBeOfType<uint>();
}
/// <summary>Verifies that ParseValue Structure is refused.</summary>
[Fact]
public void ParseValue_Structure_refused()
{
@@ -122,6 +141,7 @@ public sealed class WriteCommandParseValueTests
() => WriteCommand.ParseValue("42", TwinCATDataType.Structure));
}
/// <summary>Verifies that ParseValue non-numeric for numeric types throws.</summary>
[Fact]
public void ParseValue_non_numeric_for_numeric_types_throws()
{
@@ -129,6 +149,10 @@ public sealed class WriteCommandParseValueTests
() => WriteCommand.ParseValue("xyz", TwinCATDataType.DInt));
}
/// <summary>Verifies that SynthesiseTagName preserves symbolic path verbatim.</summary>
/// <param name="symbol">The symbolic path to synthesise.</param>
/// <param name="type">The TwinCAT data type.</param>
/// <param name="expected">The expected synthesised tag name.</param>
[Theory]
[InlineData("MAIN.bStart", TwinCATDataType.Bool, "MAIN.bStart:Bool")]
[InlineData("GVL.Counter", TwinCATDataType.DInt, "GVL.Counter:DInt")]