using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.Cli.Common; namespace ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.Tests; [Trait("Category", "Unit")] public sealed class SnapshotFormatterTests { private static readonly DateTime FixedTime = new(2026, 4, 21, 12, 34, 56, 789, DateTimeKind.Utc); [Fact] public void Format_includes_tag_value_status_and_both_timestamps() { var snap = new DataValueSnapshot(42, 0u, FixedTime, FixedTime); var output = SnapshotFormatter.Format("N7:0", snap); output.ShouldContain("Tag: N7:0"); output.ShouldContain("Value: 42"); output.ShouldContain("Status: 0x00000000 (Good)"); output.ShouldContain("Source Time: 2026-04-21T12:34:56.789Z"); output.ShouldContain("Server Time: 2026-04-21T12:34:56.789Z"); } [Theory] // Numeric codes are the canonical OPC Foundation Opc.Ua.StatusCodes values. [InlineData(0x00000000u, "Good")] [InlineData(0x80000000u, "Bad")] [InlineData(0x80050000u, "BadCommunicationError")] [InlineData(0x800A0000u, "BadTimeout")] [InlineData(0x80310000u, "BadNoCommunication")] [InlineData(0x80320000u, "BadWaitingForInitialData")] [InlineData(0x80340000u, "BadNodeIdUnknown")] [InlineData(0x80330000u, "BadNodeIdInvalid")] [InlineData(0x80740000u, "BadTypeMismatch")] [InlineData(0x40000000u, "Uncertain")] public void FormatStatus_names_well_known_status_codes(uint status, string expectedName) { SnapshotFormatter.FormatStatus(status).ShouldBe($"0x{status:X8} ({expectedName})"); } [Theory] // Regression for Driver.Cli.Common-001: these codes were previously mapped to the // wrong names. The hex values below are what the buggy shortlist used; they must // now either resolve to their *correct* spec name or fall through to bare hex. [InlineData(0x80060000u, "BadTimeout")] // was mislabelled BadTimeout [InlineData(0x80070000u, "BadNoCommunication")] // was mislabelled BadNoCommunication [InlineData(0x80080000u, "BadWaitingForInitialData")] // was mislabelled BadWaitingForInitialData [InlineData(0x80350000u, "BadNodeIdInvalid")] // was mislabelled BadNodeIdInvalid public void FormatStatus_does_not_apply_pre_fix_wrong_names(uint status, string wrongName) { SnapshotFormatter.FormatStatus(status).ShouldNotContain(wrongName); } [Fact] public void FormatStatus_unknown_codes_fall_back_to_hex_only() { // 0xDEADBEEF isn't in the shortlist — just render the hex form, no name. SnapshotFormatter.FormatStatus(0xDEADBEEFu).ShouldBe("0xDEADBEEF"); } [Fact] public void FormatValue_renders_null_as_placeholder() { var snap = new DataValueSnapshot(null, 0x80050000u, null, FixedTime); var output = SnapshotFormatter.Format("Orphan", snap); output.ShouldContain("Value: "); output.ShouldContain("Source Time: -"); // null timestamp → dash } [Fact] public void FormatValue_formats_booleans_lowercase() { var snap = new DataValueSnapshot(true, 0u, FixedTime, FixedTime); SnapshotFormatter.Format("Coil", snap).ShouldContain("Value: true"); } [Fact] public void FormatValue_formats_floats_invariant_culture() { // Guards against non-invariant decimal separators (e.g. comma on PL locales) // that would break cross-platform log diffs. var snap = new DataValueSnapshot(3.14f, 0u, FixedTime, FixedTime); SnapshotFormatter.Format("F8:0", snap).ShouldContain("3.14"); } [Fact] public void FormatValue_quotes_strings() { var snap = new DataValueSnapshot("hello", 0u, FixedTime, FixedTime); SnapshotFormatter.Format("Msg", snap).ShouldContain("\"hello\""); } [Fact] public void FormatWrite_shows_status_with_tag_name() { var result = new WriteResult(0u); SnapshotFormatter.FormatWrite("Scratch", result) .ShouldBe("Write Scratch: 0x00000000 (Good)"); } [Fact] public void FormatTable_aligns_columns_and_includes_header_separator() { var names = new[] { "A", "LongerTag" }; var snaps = new[] { new DataValueSnapshot(1, 0u, FixedTime, FixedTime), new DataValueSnapshot(2, 0u, FixedTime, FixedTime), }; var table = SnapshotFormatter.FormatTable(names, snaps); table.ShouldContain("TAG"); table.ShouldContain("VALUE"); table.ShouldContain("STATUS"); table.ShouldContain("SOURCE TIME"); table.ShouldContain("---"); // separator row table.ShouldContain("LongerTag"); table.ShouldContain("0x00000000"); } [Fact] public void FormatTable_rejects_mismatched_lengths() { Should.Throw(() => SnapshotFormatter.FormatTable( new[] { "A", "B" }, new[] { new DataValueSnapshot(1, 0u, FixedTime, FixedTime) })); } [Fact] public void FormatTimestamp_normalises_local_kind_to_utc() { // Unspecified / Local times must land on UTC in the output — otherwise a CI box in // UTC+X would emit diffs against dev-laptop runs. var local = new DateTime(2026, 4, 21, 8, 0, 0, DateTimeKind.Local); var formatted = SnapshotFormatter.FormatTimestamp(local); formatted.ShouldEndWith("Z"); } }