Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/Szl/S7SzlParserTests.cs
2026-04-26 10:30:43 -04:00

214 lines
9.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Buffers.Binary;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.S7.Szl;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests.Szl;
/// <summary>
/// PR-S7-E1 — golden-byte tests for <see cref="S7SzlParser"/>. Each test hand-crafts a
/// structurally-valid SZL response payload (matching the layout in the Siemens function
/// manual, §"SSL-IDs") and asserts the parser projects every field the driver surfaces
/// through <c>@System.*</c>. Round-trip tests prove encode-then-decode is the identity
/// so test fixtures stay self-consistent without leaning on real PLC traffic.
/// </summary>
[Trait("Category", "Unit")]
public sealed class S7SzlParserTests
{
[Fact]
public void ParseCpuInfo_decodes_module_identification_records()
{
// Hand-craft a SZL 0x0011 response with three records:
// index 0x0001 — MLFB "6ES7 516-3AN01-0AB0 " (20 bytes ASCII)
// index 0x0006 — firmware Vmajor.minor.patch encoded in Ausbg1/Ausbg2
// index 0x0007 — friendly CPU name "CPU 1516-3 PN/DP"
var info = new S7CpuInfo(
CpuType: "CPU 1516-3 PN/DP",
Firmware: "V2.9.4",
OrderNo: "6ES7 516-3AN01-0AB0");
var payload = S7SzlParser.EncodeCpuInfo(info);
var parsed = S7SzlParser.ParseCpuInfo(payload);
parsed.OrderNo.ShouldBe("6ES7 516-3AN01-0AB0");
parsed.CpuType.ShouldBe("CPU 1516-3 PN/DP");
parsed.Firmware.ShouldBe("V2.9.4");
}
[Fact]
public void ParseCpuInfo_handles_missing_records_with_unknown_fallback()
{
// Header claims zero records — every field falls back to "(unknown)" rather than throwing.
var buf = new byte[S7SzlParser.HeaderLength];
BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(0, 2), S7SzlIds.ModuleIdentification);
BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(2, 2), 0);
BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(4, 2), 28); // record length valid, count = 0
BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(6, 2), 0);
var parsed = S7SzlParser.ParseCpuInfo(buf);
parsed.CpuType.ShouldBe("(unknown)");
parsed.Firmware.ShouldBe("(unknown)");
parsed.OrderNo.ShouldBe("(unknown)");
}
[Fact]
public void ParseCycleStats_decodes_min_max_avg_milliseconds()
{
// Hand-craft SZL 0x0132 with avg=10ms, min=5ms, max=42ms.
var stats = new S7CycleStats(MinMs: 5, MaxMs: 42, AvgMs: 10);
var payload = S7SzlParser.EncodeCycleStats(stats);
var parsed = S7SzlParser.ParseCycleStats(payload);
parsed.MinMs.ShouldBe(5);
parsed.MaxMs.ShouldBe(42);
parsed.AvgMs.ShouldBe(10);
}
[Fact]
public void ParseDiagBuffer_decodes_five_entries_with_timestamps_and_event_ids()
{
var entries = new List<S7DiagBufferEntry>
{
new(new DateTimeOffset(2024, 1, 15, 8, 30, 0, TimeSpan.Zero), 0x113A, 1, "Event 0x113A (priority 1)"),
new(new DateTimeOffset(2024, 1, 15, 8, 31, 5, TimeSpan.Zero), 0x4302, 5, "Event 0x4302 (priority 5)"),
new(new DateTimeOffset(2024, 1, 15, 8, 32, 17, TimeSpan.Zero), 0x4308, 5, "Event 0x4308 (priority 5)"),
new(new DateTimeOffset(2024, 1, 15, 8, 33, 42, TimeSpan.Zero), 0x39C0, 26, "Event 0x39C0 (priority 26)"),
new(new DateTimeOffset(2024, 1, 15, 8, 34, 59, TimeSpan.Zero), 0x4505, 1, "Event 0x4505 (priority 1)"),
};
var payload = S7SzlParser.EncodeDiagBuffer(entries);
var parsed = S7SzlParser.ParseDiagBuffer(payload, maxEntries: 10);
parsed.Count.ShouldBe(5);
parsed[0].EventId.ShouldBe((ushort)0x113A);
parsed[0].Priority.ShouldBe((byte)1);
parsed[0].OccurrenceUtc.Year.ShouldBe(2024);
parsed[0].OccurrenceUtc.Month.ShouldBe(1);
parsed[0].OccurrenceUtc.Day.ShouldBe(15);
parsed[0].OccurrenceUtc.Hour.ShouldBe(8);
parsed[0].OccurrenceUtc.Minute.ShouldBe(30);
parsed[3].EventId.ShouldBe((ushort)0x39C0);
parsed[3].Priority.ShouldBe((byte)26);
parsed[3].OccurrenceUtc.Second.ShouldBe(42);
}
[Fact]
public void ParseDiagBuffer_caps_entries_to_caller_supplied_max()
{
var entries = new List<S7DiagBufferEntry>();
for (var i = 0; i < 10; i++)
entries.Add(new(DateTimeOffset.UnixEpoch, (ushort)(0x1000 + i), 1, ""));
var payload = S7SzlParser.EncodeDiagBuffer(entries);
var parsed = S7SzlParser.ParseDiagBuffer(payload, maxEntries: 3);
parsed.Count.ShouldBe(3);
parsed[0].EventId.ShouldBe((ushort)0x1000);
parsed[2].EventId.ShouldBe((ushort)0x1002);
}
[Fact]
public void ParseCpuInfo_throws_on_truncated_payload()
{
// Header claims 28-byte records × 3 but body is only 4 bytes — should reject.
var buf = new byte[S7SzlParser.HeaderLength + 4];
BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(0, 2), S7SzlIds.ModuleIdentification);
BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(4, 2), 28);
BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(6, 2), 3);
Should.Throw<ArgumentException>(() => S7SzlParser.ParseCpuInfo(buf));
}
[Fact]
public void ParseCycleStats_throws_on_short_record()
{
// Record length advertised as 8 bytes — too short for the cycle-time payload.
var buf = new byte[S7SzlParser.HeaderLength + 8];
BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(0, 2), S7SzlIds.CpuStatusData);
BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(4, 2), 8);
BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(6, 2), 1);
Should.Throw<ArgumentException>(() => S7SzlParser.ParseCycleStats(buf));
}
[Fact]
public void ParseDiagBuffer_throws_on_wrong_record_length()
{
// SZL 0x00A0 records are exactly 20 bytes; 16 should be rejected.
var buf = new byte[S7SzlParser.HeaderLength + 16];
BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(0, 2), S7SzlIds.DiagnosticBuffer);
BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(4, 2), 16);
BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(6, 2), 1);
Should.Throw<ArgumentException>(() => S7SzlParser.ParseDiagBuffer(buf, maxEntries: 1));
}
[Fact]
public void Parser_throws_on_header_only_truncation()
{
Should.Throw<ArgumentException>(() => S7SzlParser.ParseCpuInfo(new byte[4]));
Should.Throw<ArgumentException>(() => S7SzlParser.ParseCycleStats(new byte[2]));
Should.Throw<ArgumentException>(() => S7SzlParser.ParseDiagBuffer(new byte[3], maxEntries: 1));
}
[Fact]
public void Round_trip_encode_then_parse_preserves_cpu_info()
{
var original = new S7CpuInfo("CPU 1215C", "V4.5.0", "6ES7 215-1AG40-0XB0");
var enc = S7SzlParser.EncodeCpuInfo(original);
var dec = S7SzlParser.ParseCpuInfo(enc);
dec.CpuType.ShouldBe(original.CpuType);
dec.Firmware.ShouldBe(original.Firmware);
dec.OrderNo.ShouldBe(original.OrderNo);
// Re-encode the parsed result and parse again — must equal the first decode.
var reenc = S7SzlParser.EncodeCpuInfo(dec);
var redec = S7SzlParser.ParseCpuInfo(reenc);
redec.ShouldBe(dec);
}
[Fact]
public void Round_trip_encode_then_parse_preserves_cycle_stats()
{
var original = new S7CycleStats(MinMs: 1, MaxMs: 999, AvgMs: 7);
var enc = S7SzlParser.EncodeCycleStats(original);
var dec = S7SzlParser.ParseCycleStats(enc);
dec.ShouldBe(original);
var reenc = S7SzlParser.EncodeCycleStats(dec);
var redec = S7SzlParser.ParseCycleStats(reenc);
redec.ShouldBe(dec);
}
[Fact]
public void Round_trip_encode_then_parse_preserves_diag_buffer_event_ids_and_priority()
{
// Use UTC midnight aligned timestamps so the BCD encoder's second-precision rounding
// (ms isn't round-tripped) doesn't cause a comparison miss.
var original = new[]
{
new S7DiagBufferEntry(new DateTimeOffset(2024, 6, 1, 12, 0, 0, TimeSpan.Zero), 0xAAAA, 5, "Event 0xAAAA (priority 5)"),
new S7DiagBufferEntry(new DateTimeOffset(2024, 6, 2, 0, 30, 15, TimeSpan.Zero), 0xBBBB, 10, "Event 0xBBBB (priority 10)"),
};
var enc = S7SzlParser.EncodeDiagBuffer(original);
var dec = S7SzlParser.ParseDiagBuffer(enc, maxEntries: 10);
dec.Count.ShouldBe(2);
// EventId / Priority round-trip exactly; OccurrenceUtc round-trips at second precision.
dec[0].EventId.ShouldBe(original[0].EventId);
dec[0].Priority.ShouldBe(original[0].Priority);
dec[0].OccurrenceUtc.ShouldBe(original[0].OccurrenceUtc);
dec[1].EventId.ShouldBe(original[1].EventId);
dec[1].OccurrenceUtc.ShouldBe(original[1].OccurrenceUtc);
// Re-encode + re-parse should yield the same decoded list.
var reenc = S7SzlParser.EncodeDiagBuffer(dec);
var redec = S7SzlParser.ParseDiagBuffer(reenc, maxEntries: 10);
redec.Count.ShouldBe(dec.Count);
for (var i = 0; i < dec.Count; i++)
{
redec[i].EventId.ShouldBe(dec[i].EventId);
redec[i].Priority.ShouldBe(dec[i].Priority);
redec[i].OccurrenceUtc.ShouldBe(dec[i].OccurrenceUtc);
}
}
}