test(driver-s7): resolve Medium code-review finding (Driver.S7-014)

Add S7TypeMappingTests.cs covering ReinterpretRawValue and BoxValueForWrite —
26 tests verifying every implemented type round-trip (Bool/Byte/UInt16/Int16/
UInt32/Int32/Float32), two's-complement reinterpret semantics (ushort→short,
uint→int), unsupported-type NotSupportedException, and overflow edge cases.
These methods were factored out as internal static in the S7-002/S7-008 commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-22 10:17:15 -04:00
parent 909490622d
commit aeb5fc48ae

View File

@@ -0,0 +1,173 @@
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests;
/// <summary>
/// Unit tests for <see cref="S7Driver.ReinterpretRawValue"/> and
/// <see cref="S7Driver.BoxValueForWrite"/> — the pure read/write type-reinterpret
/// helpers factored out of <c>ReadOneAsync</c> / <c>WriteOneAsync</c> so they can be
/// exercised without a live PLC (Driver.S7-014).
/// </summary>
[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<NotSupportedException>(() =>
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<NotSupportedException>(() =>
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<OverflowException>(() => S7Driver.BoxValueForWrite(S7DataType.Byte, 256));
}
[Fact]
public void BoxValueForWrite_UInt16_overflows_for_out_of_range_value()
{
Should.Throw<OverflowException>(() => S7Driver.BoxValueForWrite(S7DataType.UInt16, 65_536));
}
}