fix(driver-cli-common): resolve Medium code-review finding (Driver.Cli.Common-002)

FormatStatus now matches named codes against code & 0xFFFF0000 (high-word
mask) rather than exact equality, so status codes carrying sub-code or flag
bits in the low 16 bits (e.g. 0x80050001) still resolve to their named class.
For codes not in the named shortlist a severity-class fallback using the top
2 bits always emits Good / Uncertain / Bad rather than bare hex.

Updated the stale FormatStatus_unknown_codes_fall_back_to_hex_only test (its
expectation became invalid once the severity-class fallback was added) and
added new Theory cases exercising both the high-word matching and the
severity-class fallback paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-22 08:37:47 -04:00
parent 3d8c285034
commit 1433a1cf30
3 changed files with 145 additions and 12 deletions

View File

@@ -1,3 +1,4 @@
using CliFx.Infrastructure;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
@@ -55,10 +56,11 @@ public sealed class SnapshotFormatterTests
}
[Fact]
public void FormatStatus_unknown_codes_fall_back_to_hex_only()
public void FormatStatus_unknown_codes_fall_back_to_severity_class()
{
// 0xDEADBEEF isn't in the shortlist — just render the hex form, no name.
SnapshotFormatter.FormatStatus(0xDEADBEEFu).ShouldBe("0xDEADBEEF");
// 0xDEADBEEF isn't in the named shortlist. Since -002 was fixed the severity
// fallback (bit 31 = 1 → "Bad") applies, so operators always see a quality label.
SnapshotFormatter.FormatStatus(0xDEADBEEFu).ShouldBe("0xDEADBEEF (Bad)");
}
[Fact]
@@ -138,4 +140,118 @@ public sealed class SnapshotFormatterTests
var formatted = SnapshotFormatter.FormatTimestamp(local);
formatted.ShouldEndWith("Z");
}
// --- Driver.Cli.Common-002: sub-code bits in status codes ---
[Theory]
// Status codes with non-zero low-word flag bits must still resolve to the named
// high-word class (Driver.Cli.Common-002).
[InlineData(0x00000001u, "Good")] // Good + info bit
[InlineData(0x80050001u, "BadCommunicationError")] // BadCommunicationError + sub-bit
[InlineData(0x800A0010u, "BadTimeout")] // BadTimeout + sub-bits
[InlineData(0x40000080u, "Uncertain")] // Uncertain + info bit
public void FormatStatus_with_sub_code_bits_resolves_to_named_class(uint statusCode, string expectedName)
{
SnapshotFormatter.FormatStatus(statusCode).ShouldContain($"({expectedName})");
}
[Theory]
// Unknown sub-codes fall back to the severity class (Good / Uncertain / Bad).
[InlineData(0x80990000u, "Bad")] // Unknown bad sub-code → "Bad"
[InlineData(0x80990001u, "Bad")] // Unknown bad sub-code + flag bit → "Bad"
[InlineData(0x40990000u, "Uncertain")] // Unknown uncertain sub-code → "Uncertain"
[InlineData(0x00990000u, "Good")] // Unknown good sub-code → "Good"
public void FormatStatus_unknown_sub_code_falls_back_to_severity_class(uint statusCode, string expectedSeverity)
{
SnapshotFormatter.FormatStatus(statusCode).ShouldContain($"({expectedSeverity})");
}
// --- FormatTable empty-input ---
[Fact]
public void FormatTable_with_empty_input_returns_header_only()
{
// A batch read that returns zero tags must not throw — it should emit just the
// header + separator rows (Driver.Cli.Common-004 / Driver.Cli.Common-005).
var table = SnapshotFormatter.FormatTable(
Array.Empty<string>(),
Array.Empty<DataValueSnapshot>());
table.ShouldContain("TAG");
table.ShouldContain("VALUE");
table.ShouldContain("STATUS");
table.ShouldContain("SOURCE TIME");
table.ShouldContain("---");
}
}
[Trait("Category", "Unit")]
public sealed class DriverCommandBaseTests
{
/// <summary>
/// Minimal concrete subclass used only for testing the base class helpers.
/// </summary>
[CliFx.Attributes.Command("test-stub", Description = "Test stub — not a real command.")]
private sealed class TestCommand : DriverCommandBase
{
public override TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(1);
public override ValueTask ExecuteAsync(IConsole console) => default;
// Expose protected methods for testing.
public void InvokeConfigureLogging() => ConfigureLogging();
public static void InvokeFlushLogging() => FlushLogging();
}
[Fact]
public void ConfigureLogging_non_verbose_sets_warning_level()
{
var cmd = new TestCommand { Verbose = false };
cmd.InvokeConfigureLogging();
// At Warning level, Debug writes must not flow.
Serilog.Log.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug).ShouldBeFalse();
Serilog.Log.Logger.IsEnabled(Serilog.Events.LogEventLevel.Warning).ShouldBeTrue();
DriverCommandBase_TestCommand_Teardown();
}
[Fact]
public void ConfigureLogging_verbose_sets_debug_level()
{
var cmd = new TestCommand { Verbose = true };
cmd.InvokeConfigureLogging();
Serilog.Log.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug).ShouldBeTrue();
DriverCommandBase_TestCommand_Teardown();
}
[Fact]
public void ConfigureLogging_is_idempotent_second_call_is_noop()
{
// First call sets verbose=false (Warning); second call with verbose=true must not
// reconfigure — the guard makes it a no-op.
var cmd = new TestCommand { Verbose = false };
cmd.InvokeConfigureLogging();
var loggerAfterFirst = Serilog.Log.Logger;
// A new instance would normally apply its own Verbose=true setting, but here we
// reuse the same instance so the guard field fires.
cmd.InvokeConfigureLogging(); // no-op
Serilog.Log.Logger.ShouldBeSameAs(loggerAfterFirst);
DriverCommandBase_TestCommand_Teardown();
}
[Fact]
public void FlushLogging_does_not_throw()
{
// After CloseAndFlush the static logger is replaced with a silent logger;
// verify the call itself does not throw.
TestCommand.InvokeFlushLogging();
}
/// <summary>
/// Resets the global Serilog logger so tests do not bleed into each other.
/// </summary>
private static void DriverCommandBase_TestCommand_Teardown() =>
Serilog.Log.CloseAndFlush();
}