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:
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user