Files
lmxopcua/tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.Tests/AbCipCommandBaseTests.cs
T
Joseph Doherty 64e3fbe035
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
docs: backfill XML documentation across 756 files
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.
2026-05-28 08:10:17 -04:00

255 lines
8.4 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.Commands;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.Tests;
/// <summary>
/// Covers <see cref="AbCipCommandBase"/>: the shared <c>BuildOptions</c> projection
/// (driver-options mapping the four commands depend on), the <c>RejectStructure</c>
/// guard, the <c>Timeout</c> override behaviour, and <c>TimeoutMs</c> validation.
/// </summary>
[Trait("Category", "Unit")]
public sealed class AbCipCommandBaseTests
{
/// <summary>
/// Local subclass that surfaces the protected helpers + properties under test.
/// </summary>
[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;
}
private static AbCipTagDefinition SampleTag(string name = "Motor01") => new(
Name: name,
DeviceHostAddress: "ab://10.0.0.5/1,0",
TagPath: "Motor01",
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()
{
var cmd = new TestableCommand
{
Gateway = "ab://10.0.0.5/1,0",
Family = AbCipPlcFamily.ControlLogix,
TimeoutMs = 5000,
};
var options = cmd.InvokeBuildOptions([SampleTag()]);
options.Probe.Enabled.ShouldBeFalse();
}
/// <summary>
/// Verifies that BuildOptions disables controller browse.
/// </summary>
[Fact]
public void BuildOptions_disables_controller_browse()
{
var cmd = new TestableCommand
{
Gateway = "ab://10.0.0.5/1,0",
Family = AbCipPlcFamily.ControlLogix,
TimeoutMs = 5000,
};
var options = cmd.InvokeBuildOptions([SampleTag()]);
options.EnableControllerBrowse.ShouldBeFalse();
}
/// <summary>
/// Verifies that BuildOptions disables alarm projection.
/// </summary>
[Fact]
public void BuildOptions_disables_alarm_projection()
{
var cmd = new TestableCommand
{
Gateway = "ab://10.0.0.5/1,0",
Family = AbCipPlcFamily.ControlLogix,
TimeoutMs = 5000,
};
var options = cmd.InvokeBuildOptions([SampleTag()]);
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()
{
var cmd = new TestableCommand
{
Gateway = "ab://10.0.0.5/1,0",
Family = AbCipPlcFamily.CompactLogix,
TimeoutMs = 5000,
};
var options = cmd.InvokeBuildOptions([SampleTag()]);
options.Devices.Count.ShouldBe(1);
var device = options.Devices[0];
device.HostAddress.ShouldBe("ab://10.0.0.5/1,0");
device.PlcFamily.ShouldBe(AbCipPlcFamily.CompactLogix);
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()
{
var tags = new[] { SampleTag("t1"), SampleTag("t2") };
var cmd = new TestableCommand
{
Gateway = "ab://10.0.0.5/1,0",
Family = AbCipPlcFamily.ControlLogix,
TimeoutMs = 5000,
};
var options = cmd.InvokeBuildOptions(tags);
options.Tags.Count.ShouldBe(2);
options.Tags[0].Name.ShouldBe("t1");
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()
{
var cmd = new TestableCommand
{
Gateway = "ab://10.0.0.5/1,0",
Family = AbCipPlcFamily.ControlLogix,
TimeoutMs = 7500,
};
var options = cmd.InvokeBuildOptions([SampleTag()]);
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()
{
var cmd = new TestableCommand
{
Gateway = "ab://10.0.0.5/1,0",
Family = AbCipPlcFamily.ControlLogix,
};
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()
{
// Driver.AbCip.Cli-006 — the empty init body would silently discard an
// object-initializer assignment, hiding a "driven by TimeoutMs" misuse. The fix
// makes it fail-fast with NotSupportedException so the contract is explicit.
Should.Throw<NotSupportedException>(() => new TestableCommand
{
Gateway = "ab://10.0.0.5/1,0",
Timeout = TimeSpan.FromSeconds(99),
});
}
/// <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)]
public void Timeout_get_throws_CommandException_when_TimeoutMs_is_non_positive(int badMs)
{
// Driver.AbCip.Cli-004 — TimeoutMs must be > 0. Validation is exposed via the
// Timeout getter so any command path that touches Timeout sees the same guard.
var cmd = new TestableCommand
{
Gateway = "ab://10.0.0.5/1,0",
TimeoutMs = badMs,
};
var ex = Should.Throw<CliFx.Exceptions.CommandException>(() => _ = cmd.Timeout);
ex.Message.ShouldContain("--timeout-ms");
}
/// <summary>
/// Verifies that RejectStructure throws for Structure DataType.
/// </summary>
[Fact]
public void RejectStructure_throws_for_Structure_DataType()
{
var ex = Should.Throw<CliFx.Exceptions.CommandException>(
() => CallRejectStructure(AbCipDataType.Structure));
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)]
[InlineData(AbCipDataType.Real)]
public void RejectStructure_passes_for_atomic_types(AbCipDataType type)
{
// No throw — atomic types are allowed.
Should.NotThrow(() => CallRejectStructure(type));
}
// The static helper is protected; reflect to it once so the test stays at AbCipCommandBase.
private static void CallRejectStructure(AbCipDataType type)
{
var method = typeof(AbCipCommandBase).GetMethod(
"RejectStructure",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)
?? throw new InvalidOperationException("RejectStructure not found");
try
{
method.Invoke(null, [type]);
}
catch (System.Reflection.TargetInvocationException tie) when (tie.InnerException is not null)
{
throw tie.InnerException;
}
}
}