diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7TypeMappingTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7TypeMappingTests.cs new file mode 100644 index 0000000..bcaf395 --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7TypeMappingTests.cs @@ -0,0 +1,173 @@ +using Shouldly; +using Xunit; + +namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests; + +/// +/// Unit tests for and +/// — the pure read/write type-reinterpret +/// helpers factored out of ReadOneAsync / WriteOneAsync so they can be +/// exercised without a live PLC (Driver.S7-014). +/// +[Trait("Category", "Unit")] +public sealed class S7TypeMappingTests +{ + // ── Helpers ────────────────────────────────────────────────────────────────────────────── + + private static S7TagDefinition Tag(string name, S7DataType dt) => + new(name, "DB1.DBW0", dt); + + private static S7ParsedAddress Addr(S7Size size) => + new(S7Area.DataBlock, DbNumber: 1, size, ByteOffset: 0, BitOffset: 0); + + // ── ReinterpretRawValue — implemented types ─────────────────────────────────────────── + + [Fact] + public void ReinterpretRawValue_Bool_returns_bool() + { + var tag = new S7TagDefinition("B", "DB1.DBX0.0", S7DataType.Bool); + var addr = new S7ParsedAddress(S7Area.DataBlock, 1, S7Size.Bit, 0, 0); + S7Driver.ReinterpretRawValue(tag, addr, true).ShouldBe(true); + S7Driver.ReinterpretRawValue(tag, addr, false).ShouldBe(false); + } + + [Fact] + public void ReinterpretRawValue_Byte_returns_byte() + { + var tag = new S7TagDefinition("By", "DB1.DBB0", S7DataType.Byte); + var addr = Addr(S7Size.Byte); + S7Driver.ReinterpretRawValue(tag, addr, (byte)42).ShouldBe((byte)42); + } + + [Fact] + public void ReinterpretRawValue_UInt16_returns_ushort() + { + S7Driver.ReinterpretRawValue(Tag("U16", S7DataType.UInt16), Addr(S7Size.Word), (ushort)1000) + .ShouldBe((ushort)1000); + } + + [Fact] + public void ReinterpretRawValue_Int16_reinterprets_ushort_as_signed() + { + // 0xFFFF as ushort == -1 as short (two's complement reinterpret, not Convert). + var result = S7Driver.ReinterpretRawValue(Tag("I16", S7DataType.Int16), Addr(S7Size.Word), (ushort)0xFFFF); + result.ShouldBe((short)-1); + } + + [Fact] + public void ReinterpretRawValue_UInt32_returns_uint() + { + S7Driver.ReinterpretRawValue(Tag("U32", S7DataType.UInt32), Addr(S7Size.DWord), (uint)70_000u) + .ShouldBe(70_000u); + } + + [Fact] + public void ReinterpretRawValue_Int32_reinterprets_uint_as_signed() + { + // 0xFFFFFFFF as uint == -1 as int. + var result = S7Driver.ReinterpretRawValue(Tag("I32", S7DataType.Int32), Addr(S7Size.DWord), 0xFFFF_FFFFu); + result.ShouldBe(-1); + } + + [Fact] + public void ReinterpretRawValue_Float32_converts_uint_bits_to_float() + { + float expected = 3.14f; + uint bits = BitConverter.SingleToUInt32Bits(expected); + var result = S7Driver.ReinterpretRawValue(Tag("F32", S7DataType.Float32), Addr(S7Size.DWord), bits); + ((float)result).ShouldBe(expected, tolerance: 1e-6f); + } + + // ── ReinterpretRawValue — unsupported types throw NotSupportedException ─────────────── + + [Theory] + [InlineData(S7DataType.Int64)] + [InlineData(S7DataType.UInt64)] + [InlineData(S7DataType.Float64)] + [InlineData(S7DataType.String)] + [InlineData(S7DataType.DateTime)] + public void ReinterpretRawValue_unsupported_type_throws_NotSupportedException(S7DataType dt) + { + Should.Throw(() => + S7Driver.ReinterpretRawValue(Tag("x", dt), Addr(S7Size.DWord), (uint)0)); + } + + // ── BoxValueForWrite — implemented types ───────────────────────────────────────────── + + [Fact] + public void BoxValueForWrite_Bool_converts_value_to_bool() + { + S7Driver.BoxValueForWrite(S7DataType.Bool, true).ShouldBe(true); + S7Driver.BoxValueForWrite(S7DataType.Bool, false).ShouldBe(false); + S7Driver.BoxValueForWrite(S7DataType.Bool, 1).ShouldBe(true); + } + + [Fact] + public void BoxValueForWrite_Byte_converts_to_byte() + { + S7Driver.BoxValueForWrite(S7DataType.Byte, (byte)200).ShouldBe((byte)200); + } + + [Fact] + public void BoxValueForWrite_UInt16_converts_to_ushort() + { + S7Driver.BoxValueForWrite(S7DataType.UInt16, (ushort)1234).ShouldBe((ushort)1234); + } + + [Fact] + public void BoxValueForWrite_Int16_reinterprets_as_ushort_two_complement() + { + // -1 as short → 0xFFFF as ushort; S7.Net writes the ushort to the wire. + S7Driver.BoxValueForWrite(S7DataType.Int16, (short)-1).ShouldBe((ushort)0xFFFF); + } + + [Fact] + public void BoxValueForWrite_UInt32_converts_to_uint() + { + S7Driver.BoxValueForWrite(S7DataType.UInt32, 70_000u).ShouldBe(70_000u); + } + + [Fact] + public void BoxValueForWrite_Int32_reinterprets_as_uint_two_complement() + { + // -1 as int → 0xFFFFFFFF as uint. + S7Driver.BoxValueForWrite(S7DataType.Int32, -1).ShouldBe(0xFFFF_FFFFu); + } + + [Fact] + public void BoxValueForWrite_Float32_encodes_as_ieee754_uint() + { + float f = 3.14f; + var expected = BitConverter.SingleToUInt32Bits(f); + S7Driver.BoxValueForWrite(S7DataType.Float32, f).ShouldBe(expected); + } + + // ── BoxValueForWrite — unsupported types throw NotSupportedException ───────────────── + + [Theory] + [InlineData(S7DataType.Int64)] + [InlineData(S7DataType.UInt64)] + [InlineData(S7DataType.Float64)] + [InlineData(S7DataType.String)] + [InlineData(S7DataType.DateTime)] + public void BoxValueForWrite_unsupported_type_throws_NotSupportedException(S7DataType dt) + { + Should.Throw(() => + S7Driver.BoxValueForWrite(dt, 0)); + } + + // ── BoxValueForWrite — overflow paths ──────────────────────────────────────────────── + + [Fact] + public void BoxValueForWrite_Byte_overflows_for_out_of_range_value() + { + // Convert.ToByte(256) throws OverflowException. + Should.Throw(() => S7Driver.BoxValueForWrite(S7DataType.Byte, 256)); + } + + [Fact] + public void BoxValueForWrite_UInt16_overflows_for_out_of_range_value() + { + Should.Throw(() => S7Driver.BoxValueForWrite(S7DataType.UInt16, 65_536)); + } +}