feat(s7): String (S7 STRING) scalar read+write via S7.Net.Types.S7String
This commit is contained in:
@@ -618,8 +618,11 @@ public sealed class S7Driver
|
|||||||
S7DataType.UInt64 => System.Buffers.Binary.BinaryPrimitives.ReadUInt64BigEndian(block),
|
S7DataType.UInt64 => System.Buffers.Binary.BinaryPrimitives.ReadUInt64BigEndian(block),
|
||||||
S7DataType.Float64 => System.Buffers.Binary.BinaryPrimitives.ReadDoubleBigEndian(block),
|
S7DataType.Float64 => System.Buffers.Binary.BinaryPrimitives.ReadDoubleBigEndian(block),
|
||||||
|
|
||||||
S7DataType.String => throw new NotSupportedException(
|
// S7 classic STRING: [maxLen byte][curLen byte][chars…]. S7.Net's S7String.FromByteArray
|
||||||
"S7 String scalar reads land in a follow-up PR"),
|
// reads the two-byte header and returns exactly curLen ASCII chars, ignoring the reserved
|
||||||
|
// padding past curLen — it validates curLen against the block, so no extra guard is needed.
|
||||||
|
S7DataType.String => global::S7.Net.Types.S7String.FromByteArray(block),
|
||||||
|
|
||||||
S7DataType.DateTime => throw new NotSupportedException(
|
S7DataType.DateTime => throw new NotSupportedException(
|
||||||
"S7 DateTime scalar reads land in a follow-up PR"),
|
"S7 DateTime scalar reads land in a follow-up PR"),
|
||||||
|
|
||||||
@@ -662,7 +665,13 @@ public sealed class S7Driver
|
|||||||
}
|
}
|
||||||
|
|
||||||
case S7DataType.String:
|
case S7DataType.String:
|
||||||
throw new NotSupportedException("S7 String scalar writes land in a follow-up PR");
|
// S7.Net's S7String.ToByteArray builds [maxLen=StringLength][curLen][chars…] and pads
|
||||||
|
// the result to the full reserved field (StringLength + 2 bytes) — exactly the width
|
||||||
|
// ReadScalarBlockAsync read, so WriteBytesAsync writes the whole reserved STRING. A value
|
||||||
|
// longer than StringLength throws ArgumentException (S7.Net rejects overflow; we do NOT
|
||||||
|
// silently truncate). A null value encodes as the empty string.
|
||||||
|
return global::S7.Net.Types.S7String.ToByteArray(Convert.ToString(value) ?? "", tag.StringLength);
|
||||||
|
|
||||||
case S7DataType.DateTime:
|
case S7DataType.DateTime:
|
||||||
throw new NotSupportedException("S7 DateTime scalar writes land in a follow-up PR");
|
throw new NotSupportedException("S7 DateTime scalar writes land in a follow-up PR");
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Shouldly;
|
using Shouldly;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using S7.Net.Types;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests;
|
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests;
|
||||||
|
|
||||||
@@ -10,8 +11,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests;
|
|||||||
/// Int64/UInt64/LReal (Float64) scalar from a contiguous big-endian byte block — the
|
/// Int64/UInt64/LReal (Float64) scalar from a contiguous big-endian byte block — the
|
||||||
/// network I/O half (<c>Plc.ReadBytesAsync</c>/<c>WriteBytesAsync</c>) has no in-process
|
/// network I/O half (<c>Plc.ReadBytesAsync</c>/<c>WriteBytesAsync</c>) has no in-process
|
||||||
/// fake so only the codec is unit-proven (mirrors <see cref="S7ArrayReadTests"/>).
|
/// fake so only the codec is unit-proven (mirrors <see cref="S7ArrayReadTests"/>).
|
||||||
/// String/DateTime decode is a deferred stub here (T3/T4); this file pins the
|
/// String (S7 classic STRING) decode/encode is proven here via
|
||||||
/// NotSupportedException contract those land against.
|
/// <see cref="S7.Net.Types.S7String"/>; DateTime decode is still a deferred stub (T4) and
|
||||||
|
/// this file pins the NotSupportedException contract it lands against.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Trait("Category", "Unit")]
|
[Trait("Category", "Unit")]
|
||||||
public sealed class S7ScalarBlockTests
|
public sealed class S7ScalarBlockTests
|
||||||
@@ -154,38 +156,159 @@ public sealed class S7ScalarBlockTests
|
|||||||
decoded.ShouldBeOfType<ulong>().ShouldBe(value);
|
decoded.ShouldBeOfType<ulong>().ShouldBe(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Verifies Float64 (LReal) round-trips for representative doubles.</summary>
|
/// <summary>Verifies Float64 (LReal) round-trips for representative doubles, including
|
||||||
|
/// the IEEE-754 specials (NaN / ±Infinity) — these pass through BinaryPrimitives unchanged.</summary>
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(0.0)]
|
[InlineData(0.0)]
|
||||||
[InlineData(3.141592653589793)]
|
[InlineData(3.141592653589793)]
|
||||||
[InlineData(-2.5e-300)]
|
[InlineData(-2.5e-300)]
|
||||||
[InlineData(1.7976931348623157e308)]
|
[InlineData(1.7976931348623157e308)]
|
||||||
|
[InlineData(double.NaN)]
|
||||||
|
[InlineData(double.PositiveInfinity)]
|
||||||
|
[InlineData(double.NegativeInfinity)]
|
||||||
public void Float64_round_trips(double value)
|
public void Float64_round_trips(double value)
|
||||||
{
|
{
|
||||||
var tag = Tag(S7DataType.Float64);
|
var tag = Tag(S7DataType.Float64);
|
||||||
var decoded = S7Driver.DecodeScalarBlock(tag, Addr(), S7Driver.EncodeScalarBlock(tag, value));
|
var decoded = S7Driver.DecodeScalarBlock(tag, Addr(), S7Driver.EncodeScalarBlock(tag, value));
|
||||||
decoded.ShouldBeOfType<double>().ShouldBe(value);
|
// double.NaN != double.NaN, so compare via Shouldly's IsNaN for that case.
|
||||||
|
if (double.IsNaN(value))
|
||||||
|
decoded.ShouldBeOfType<double>().ShouldBe(double.NaN);
|
||||||
|
else
|
||||||
|
decoded.ShouldBeOfType<double>().ShouldBe(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Deferred-stub contract: String/DateTime throw NotSupportedException ────────────────
|
// ── DecodeScalarBlock — String (S7 classic STRING) ────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>Verifies String/DateTime decode is a deferred stub (NotSupportedException — T3/T4).</summary>
|
/// <summary>Verifies an S7 STRING block decodes to its C# string, ignoring the
|
||||||
/// <param name="dt">The not-yet-implemented wide type.</param>
|
/// two-byte <c>[maxLen][curLen]</c> header and any reserved padding past curLen.</summary>
|
||||||
[Theory]
|
[Fact]
|
||||||
[InlineData(S7DataType.String)]
|
public void DecodeScalarBlock_String_reads_header_and_chars()
|
||||||
[InlineData(S7DataType.DateTime)]
|
|
||||||
public void DecodeScalarBlock_String_or_DateTime_throws_NotSupported(S7DataType dt)
|
|
||||||
{
|
{
|
||||||
var tag = Tag(dt, stringLength: 10);
|
// S7 classic STRING layout: [maxLen=10][curLen=5]['H']['E']['L']['L']['O'][pad…].
|
||||||
|
var block = S7String.ToByteArray("HELLO", 10);
|
||||||
|
block.Length.ShouldBe(12); // StringLength(10) + 2-byte header.
|
||||||
|
block[0].ShouldBe((byte)10); // maxLen.
|
||||||
|
block[1].ShouldBe((byte)5); // curLen.
|
||||||
|
|
||||||
|
var result = S7Driver.DecodeScalarBlock(Tag(S7DataType.String, stringLength: 10), Addr(), block);
|
||||||
|
result.ShouldBeOfType<string>().ShouldBe("HELLO");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies a hand-built STRING block (not produced by S7.Net) still decodes,
|
||||||
|
/// pinning the on-the-wire layout we depend on.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void DecodeScalarBlock_String_decodes_hand_built_block()
|
||||||
|
{
|
||||||
|
var block = new byte[12];
|
||||||
|
block[0] = 10; // maxLen.
|
||||||
|
block[1] = 5; // curLen.
|
||||||
|
block[2] = (byte)'H'; block[3] = (byte)'E'; block[4] = (byte)'L';
|
||||||
|
block[5] = (byte)'L'; block[6] = (byte)'O'; // bytes 7..11 stay zero (reserved padding).
|
||||||
|
|
||||||
|
var result = S7Driver.DecodeScalarBlock(Tag(S7DataType.String, stringLength: 10), Addr(), block);
|
||||||
|
result.ShouldBeOfType<string>().ShouldBe("HELLO");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies an empty S7 STRING decodes to the empty string.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void DecodeScalarBlock_String_empty()
|
||||||
|
{
|
||||||
|
var block = S7String.ToByteArray("", 10);
|
||||||
|
var result = S7Driver.DecodeScalarBlock(Tag(S7DataType.String, stringLength: 10), Addr(), block);
|
||||||
|
result.ShouldBeOfType<string>().ShouldBe("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── EncodeScalarBlock — String (S7 classic STRING) ────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Verifies a C# string encodes to a STRING block sized to the reserved field
|
||||||
|
/// (StringLength + 2), with header <c>[maxLen=StringLength][curLen=value.Length]</c> and
|
||||||
|
/// the chars laid out after it.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void EncodeScalarBlock_String_writes_header_chars_and_pads_to_reserved()
|
||||||
|
{
|
||||||
|
var tag = Tag(S7DataType.String, stringLength: 10);
|
||||||
|
var block = S7Driver.EncodeScalarBlock(tag, "HELLO");
|
||||||
|
|
||||||
|
block.Length.ShouldBe(12); // StringLength(10) + 2 — full reserved field, not curLen-sized.
|
||||||
|
block[0].ShouldBe((byte)10); // maxLen == declared StringLength.
|
||||||
|
block[1].ShouldBe((byte)5); // curLen == value.Length.
|
||||||
|
block[2].ShouldBe((byte)'H');
|
||||||
|
block[6].ShouldBe((byte)'O');
|
||||||
|
block[7].ShouldBe((byte)0); // reserved padding is zeroed.
|
||||||
|
block[11].ShouldBe((byte)0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies a null value encodes as an empty STRING (curLen 0), not a throw.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void EncodeScalarBlock_String_null_value_is_empty_string()
|
||||||
|
{
|
||||||
|
var tag = Tag(S7DataType.String, stringLength: 10);
|
||||||
|
var block = S7Driver.EncodeScalarBlock(tag, value: null);
|
||||||
|
|
||||||
|
block.Length.ShouldBe(12);
|
||||||
|
block[0].ShouldBe((byte)10); // maxLen.
|
||||||
|
block[1].ShouldBe((byte)0); // curLen 0.
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies a non-string value coerces via <c>Convert.ToString</c> before encoding.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void EncodeScalarBlock_String_coerces_non_string_value()
|
||||||
|
{
|
||||||
|
var tag = Tag(S7DataType.String, stringLength: 10);
|
||||||
|
var block = S7Driver.EncodeScalarBlock(tag, 1234);
|
||||||
|
|
||||||
|
var decoded = S7Driver.DecodeScalarBlock(tag, Addr(), block);
|
||||||
|
decoded.ShouldBeOfType<string>().ShouldBe("1234");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies a string longer than the reserved length throws (S7.Net rejects it;
|
||||||
|
/// we do NOT silently truncate). Pins the overflow behaviour.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void EncodeScalarBlock_String_overflow_throws()
|
||||||
|
{
|
||||||
|
var tag = Tag(S7DataType.String, stringLength: 5);
|
||||||
|
// 6 chars into a 5-char reserved field — S7.Net throws ArgumentException.
|
||||||
|
Should.Throw<ArgumentException>(() => S7Driver.EncodeScalarBlock(tag, "ABCDEF"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── String round-trip identity (encode → decode) ──────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Verifies String round-trips through encode→decode, incl. empty and max-length.</summary>
|
||||||
|
[Theory]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData("A")]
|
||||||
|
[InlineData("Hello, S7!")] // 10 chars == StringLength (at max).
|
||||||
|
[InlineData("Tag_Value 42")]
|
||||||
|
public void String_round_trips(string value)
|
||||||
|
{
|
||||||
|
// StringLength sized to fit the longest sample (12) so none overflow.
|
||||||
|
var tag = Tag(S7DataType.String, stringLength: 12);
|
||||||
|
var decoded = S7Driver.DecodeScalarBlock(tag, Addr(), S7Driver.EncodeScalarBlock(tag, value));
|
||||||
|
decoded.ShouldBeOfType<string>().ShouldBe(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies a string exactly at the reserved length round-trips (boundary, no overflow).</summary>
|
||||||
|
[Fact]
|
||||||
|
public void String_at_max_length_round_trips()
|
||||||
|
{
|
||||||
|
var tag = Tag(S7DataType.String, stringLength: 5);
|
||||||
|
var decoded = S7Driver.DecodeScalarBlock(tag, Addr(), S7Driver.EncodeScalarBlock(tag, "ABCDE"));
|
||||||
|
decoded.ShouldBeOfType<string>().ShouldBe("ABCDE");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Deferred-stub contract: DateTime throws NotSupportedException ──────────────────────
|
||||||
|
|
||||||
|
/// <summary>Verifies DateTime decode is a deferred stub (NotSupportedException — T4).</summary>
|
||||||
|
[Fact]
|
||||||
|
public void DecodeScalarBlock_DateTime_throws_NotSupported()
|
||||||
|
{
|
||||||
|
var tag = Tag(S7DataType.DateTime);
|
||||||
var block = new byte[S7Driver.ScalarByteWidth(tag)];
|
var block = new byte[S7Driver.ScalarByteWidth(tag)];
|
||||||
Should.Throw<NotSupportedException>(() => S7Driver.DecodeScalarBlock(tag, Addr(), block));
|
Should.Throw<NotSupportedException>(() => S7Driver.DecodeScalarBlock(tag, Addr(), block));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Verifies String/DateTime encode is a deferred stub (NotSupportedException — T3/T4).</summary>
|
/// <summary>Verifies DateTime encode is a deferred stub (NotSupportedException — T4).</summary>
|
||||||
/// <param name="dt">The not-yet-implemented wide type.</param>
|
[Fact]
|
||||||
[Theory]
|
public void EncodeScalarBlock_DateTime_throws_NotSupported()
|
||||||
[InlineData(S7DataType.String)]
|
=> Should.Throw<NotSupportedException>(() => S7Driver.EncodeScalarBlock(Tag(S7DataType.DateTime), "x"));
|
||||||
[InlineData(S7DataType.DateTime)]
|
|
||||||
public void EncodeScalarBlock_String_or_DateTime_throws_NotSupported(S7DataType dt)
|
|
||||||
=> Should.Throw<NotSupportedException>(() => S7Driver.EncodeScalarBlock(Tag(dt), "x"));
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user