using Mbproxy.Bcd; using Shouldly; using Xunit; namespace Mbproxy.Tests.Bcd; /// /// Unit tests for — the allocation-free BCD nibble codec. /// /// NOTE on allocation profile: /// BcdCodec is a purely static class operating on value types (ushort, int, tuples). /// It allocates only when constructing exception objects (the error path), never on /// the success path. TryGet / hot-path decode callers in Phase 04 will be /// allocation-free for valid BCD registers. /// [Trait("Category", "Unit")] public sealed class BcdCodecTests { // ── Encode16 ──────────────────────────────────────────────────────────── [Fact] public void Encode16_1234_Returns_0x1234() => BcdCodec.Encode16(1234).ShouldBe((ushort)0x1234); [Fact] public void Encode16_0_Returns_0x0000() => BcdCodec.Encode16(0).ShouldBe((ushort)0x0000); [Fact] public void Encode16_9999_Returns_0x9999() => BcdCodec.Encode16(9999).ShouldBe((ushort)0x9999); [Fact] public void Encode16_10000_Throws_OutOfRange() { Should.Throw(() => BcdCodec.Encode16(10_000)) .ParamName.ShouldBe("value"); } [Fact] public void Encode16_Negative_Throws_OutOfRange() { Should.Throw(() => BcdCodec.Encode16(-1)) .ParamName.ShouldBe("value"); } // ── Decode16 ──────────────────────────────────────────────────────────── [Fact] public void Decode16_0x1234_Returns_1234() => BcdCodec.Decode16(0x1234).ShouldBe(1234); [Fact] public void Decode16_0x0000_Returns_0() => BcdCodec.Decode16(0x0000).ShouldBe(0); [Fact] public void Decode16_0x9999_Returns_9999() => BcdCodec.Decode16(0x9999).ShouldBe(9999); [Fact] public void Decode16_0x123A_Throws_Format() { // Nibble 'A' (10) is not a valid BCD digit; message must contain the raw hex value. var ex = Should.Throw(() => BcdCodec.Decode16(0x123A)); ex.Message.ShouldContain("0x123A", Case.Insensitive); } [Fact] public void Decode16_0x12FA_TwoBadNibbles_Throws_Format() { // Two bad nibbles in one register — still throws once with the raw value. var ex = Should.Throw(() => BcdCodec.Decode16(0x12FA)); ex.Message.ShouldContain("0x12FA", Case.Insensitive); } // ── Encode32 ──────────────────────────────────────────────────────────── [Fact] public void Encode32_12345678_Returns_LowHigh_5678_1234() { var (low, high) = BcdCodec.Encode32(12_345_678); low.ShouldBe((ushort)0x5678); high.ShouldBe((ushort)0x1234); } [Fact] public void Encode32_0_Returns_LowHigh_0_0() { var (low, high) = BcdCodec.Encode32(0); low.ShouldBe((ushort)0x0000); high.ShouldBe((ushort)0x0000); } [Fact] public void Encode32_99999999_Returns_LowHigh_9999_9999() { var (low, high) = BcdCodec.Encode32(99_999_999); low.ShouldBe((ushort)0x9999); high.ShouldBe((ushort)0x9999); } [Fact] public void Encode32_100000000_Throws_OutOfRange() { Should.Throw(() => BcdCodec.Encode32(100_000_000)) .ParamName.ShouldBe("value"); } // ── Decode32 ──────────────────────────────────────────────────────────── [Fact] public void Decode32_LowHigh_5678_1234_Returns_12345678() => BcdCodec.Decode32(0x5678, 0x1234).ShouldBe(12_345_678); [Fact] public void Decode32_BadNibble_InLow_Throws() { // Low word has a bad nibble; Decode32 must propagate the FormatException. Should.Throw(() => BcdCodec.Decode32(0xABCD, 0x1234)); } [Fact] public void Decode32_BadNibble_InHigh_Throws() { Should.Throw(() => BcdCodec.Decode32(0x5678, 0xABCD)); } // ── Round-trip 16-bit ──────────────────────────────────────────────────── /// /// Dense round-trip: boundary values plus every 100th value in [0, 9999]. /// Ensures Decode16(Encode16(v)) == v for all practical inputs. /// [Theory] [MemberData(nameof(RoundTrip16Values))] public void RoundTrip16_AllValuesUnder10000(int value) => BcdCodec.Decode16(BcdCodec.Encode16(value)).ShouldBe(value); public static IEnumerable RoundTrip16Values() { // Every 100th value (0, 100, 200, … 9900) — covers 0 as boundary automatically for (int v = 0; v <= 9999; v += 100) yield return [v]; // Additional boundary values not already hit by the stride-100 loop yield return [1]; yield return [9]; yield return [99]; yield return [999]; yield return [9999]; // Some spot-check midpoints yield return [1234]; yield return [5678]; yield return [4321]; } // ── Round-trip 32-bit ──────────────────────────────────────────────────── [Theory] [InlineData(0)] [InlineData(1)] [InlineData(9999)] [InlineData(10_000)] [InlineData(99_999_999)] [InlineData(12_345_678)] [InlineData(5_000_000)] public void RoundTrip32_RepresentativeValues(int value) { var (low, high) = BcdCodec.Encode32(value); BcdCodec.Decode32(low, high).ShouldBe(value); } }