214 lines
9.2 KiB
C#
214 lines
9.2 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|