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
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.
255 lines
8.4 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|