b50fd6c34a
Re-review at 7286d320. -008 (Low): all four commands now FlushLogging() in finally (parity
with AbCip.Cli; subscribe could drop shutdown log lines) + IL-inspection test.
153 lines
7.2 KiB
C#
153 lines
7.2 KiB
C#
using System.Linq;
|
|
using System.Reflection;
|
|
using CliFx.Attributes;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Commands;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Cli.Common;
|
|
|
|
/// <summary>
|
|
/// Locks in the CLI command-option contract surface area — short aliases and
|
|
/// help-text wording — that the AbLegacy CLI is expected to keep in parity with
|
|
/// its sibling AbCip CLI and with <c>docs/Driver.AbLegacy.Cli.md</c>.
|
|
/// Regression coverage for findings Driver.AbLegacy.Cli-002, -005, -006.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class CommandMetadataTests
|
|
{
|
|
private static CommandOptionAttribute GetOption<TCommand>(string propertyName)
|
|
{
|
|
var prop = typeof(TCommand).GetProperty(
|
|
propertyName,
|
|
BindingFlags.Public | BindingFlags.Instance);
|
|
prop.ShouldNotBeNull($"property {propertyName} is missing from {typeof(TCommand).Name}");
|
|
var attr = prop!.GetCustomAttribute<CommandOptionAttribute>();
|
|
attr.ShouldNotBeNull(
|
|
$"property {propertyName} on {typeof(TCommand).Name} lacks [CommandOption]");
|
|
return attr!;
|
|
}
|
|
|
|
// ---------- 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()
|
|
{
|
|
// Parity with read / write / subscribe: --type / -t works everywhere.
|
|
var attr = GetOption<ProbeCommand>(nameof(ProbeCommand.DataType));
|
|
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))]
|
|
[InlineData(typeof(SubscribeCommand), nameof(SubscribeCommand.DataType))]
|
|
public void Other_commands_keep_type_short_alias_t(System.Type commandType, string propName)
|
|
{
|
|
var prop = commandType.GetProperty(propName, BindingFlags.Public | BindingFlags.Instance);
|
|
prop.ShouldNotBeNull();
|
|
var attr = prop!.GetCustomAttribute<CommandOptionAttribute>();
|
|
attr.ShouldNotBeNull();
|
|
attr!.ShortName.ShouldBe('t');
|
|
}
|
|
|
|
// ---------- 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()
|
|
{
|
|
// ParseBool accepts true/false, 1/0, on/off, yes/no — the help text must say so
|
|
// (DriverClis.md documents the full alias set as the shared CLI contract).
|
|
var attr = GetOption<WriteCommand>(nameof(WriteCommand.Value));
|
|
attr.Description.ShouldNotBeNull();
|
|
attr.Description!.ShouldContain("true/false", Case.Insensitive);
|
|
attr.Description!.ShouldContain("1/0");
|
|
attr.Description!.ShouldContain("on/off", Case.Insensitive);
|
|
attr.Description!.ShouldContain("yes/no", Case.Insensitive);
|
|
}
|
|
|
|
// ---------- 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()
|
|
{
|
|
// Parity with AbCip CLI: operators passing -i 100 deserve a heads-up that
|
|
// PollGroupEngine floors sub-250ms values.
|
|
var attr = GetOption<SubscribeCommand>(nameof(SubscribeCommand.IntervalMs));
|
|
attr.Description.ShouldNotBeNull();
|
|
attr.Description!.ShouldContain("250", Case.Insensitive);
|
|
}
|
|
|
|
// ---------- Driver.AbLegacy.Cli-008 — all commands must call FlushLogging() in their finally ----------
|
|
|
|
/// <summary>
|
|
/// Verifies that every AbLegacy CLI command's async state-machine references
|
|
/// <see cref="DriverCommandBase.FlushLogging"/> so Serilog output emitted during
|
|
/// driver shutdown is flushed before process exit (regression for
|
|
/// Driver.AbLegacy.Cli-008, matching the fix applied to AbCip CLI Driver.AbCip.Cli-005).
|
|
/// </summary>
|
|
/// <param name="commandType">The command type whose state machine is inspected.</param>
|
|
[Theory]
|
|
[InlineData(typeof(ProbeCommand))]
|
|
[InlineData(typeof(ReadCommand))]
|
|
[InlineData(typeof(WriteCommand))]
|
|
[InlineData(typeof(SubscribeCommand))]
|
|
public void ExecuteAsync_calls_FlushLogging_in_state_machine(System.Type commandType)
|
|
{
|
|
// C# async methods are compiled into a nested '<ExecuteAsync>d__N' state machine
|
|
// class with a MoveNext() method that contains the actual IL. We scan the
|
|
// MoveNext() body for a call token that resolves to DriverCommandBase.FlushLogging
|
|
// using the module's metadata.
|
|
var flushMethod = typeof(DriverCommandBase)
|
|
.GetMethod("FlushLogging",
|
|
BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.FlattenHierarchy);
|
|
flushMethod.ShouldNotBeNull("DriverCommandBase.FlushLogging must exist");
|
|
|
|
// Locate the compiler-generated state-machine nested type for ExecuteAsync.
|
|
var stateMachine = commandType
|
|
.GetNestedTypes(BindingFlags.NonPublic)
|
|
.FirstOrDefault(t => t.Name.Contains("ExecuteAsync"));
|
|
stateMachine.ShouldNotBeNull(
|
|
$"{commandType.Name} must have a compiler-generated ExecuteAsync state machine");
|
|
|
|
var moveNext = stateMachine!
|
|
.GetMethod("MoveNext", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
moveNext.ShouldNotBeNull("MoveNext must exist on the state machine");
|
|
|
|
var body = moveNext!.GetMethodBody();
|
|
body.ShouldNotBeNull("MoveNext must have an inspectable method body");
|
|
|
|
// Walk the IL stream: call/callvirt opcodes (0x28 / 0x6F) are followed by a
|
|
// 4-byte metadata token. Check whether any token resolves to FlushLogging.
|
|
var il = body!.GetILAsByteArray();
|
|
il.ShouldNotBeNull();
|
|
var module = moveNext.Module;
|
|
bool found = false;
|
|
for (int i = 0; i < il!.Length - 4; i++)
|
|
{
|
|
if (il[i] != 0x28 && il[i] != 0x6F) continue; // call / callvirt
|
|
int token = il[i + 1] | (il[i + 2] << 8) | (il[i + 3] << 16) | (il[i + 4] << 24);
|
|
try
|
|
{
|
|
var resolved = module.ResolveMethod(token);
|
|
if (resolved?.Name == "FlushLogging" &&
|
|
resolved.DeclaringType == typeof(DriverCommandBase))
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
catch (ArgumentException) { /* token is a field or type, not a method */ }
|
|
}
|
|
|
|
found.ShouldBeTrue(
|
|
$"{commandType.Name}.ExecuteAsync must call FlushLogging() in its finally block " +
|
|
"so Serilog output during driver shutdown is not lost (Driver.AbLegacy.Cli-008).");
|
|
}
|
|
}
|