b085360454
Review round 1 on the golden frames: - Re-captured logix-read-dint-array16 (element i = ((i+1)<<24)|(i+1)) and logix-read-bool (= 1) so neither decode oracle is all zeros. ab_server rejects libplctag's own BOOL write with CIP 0x03 (two data bytes for a one-byte BOOL), so the BOOL is seeded by a raw UCMM Write Tag (client seed-bool). - Committed the capture client as scripts/ab-capture/client (outside the slnx, CPM off, libplctag 1.5.2 pinned); capture.sh gains a `client` subcommand. - README documents simulator-only behaviour: zeroed reply sender context, STRING typed D0 00 instead of A0 02 + handle, and the BOOL write refusal. - capture.sh no longer claims UnregisterSession reaches the wire (session ends in RST). - CaptureFileTests also check the CPF walk ends at the frame end and that every reply service is its request service | 0x80.
105 lines
3.7 KiB
C#
105 lines
3.7 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Captures;
|
|
|
|
public sealed class CaptureFileTests
|
|
{
|
|
private static readonly string[] Required =
|
|
[
|
|
"logix-register-session.hex", "logix-forward-open.hex",
|
|
"logix-read-dint.hex", "logix-read-dint-array16.hex", "logix-read-string.hex", "logix-read-bool.hex",
|
|
"logix-write-dint.hex", "logix-write-string.hex", "logix-read-unknown-tag.hex",
|
|
"logix-tags-listing.hex", "logix-forward-close.hex", "micro800-read-dint.hex",
|
|
"slc500-read-n7.hex", "slc500-write-n7.hex", "slc500-read-f8.hex",
|
|
"micrologix-read-n7.hex", "plc5-read-n7.hex",
|
|
];
|
|
|
|
public static TheoryData<string> AllCaptures()
|
|
{
|
|
var data = new TheoryData<string>();
|
|
foreach (var f in Directory.GetFiles(CaptureFile.Dir, "*.hex").Order()) data.Add(Path.GetFileName(f));
|
|
return data;
|
|
}
|
|
|
|
[Fact]
|
|
public void Every_required_capture_is_present() =>
|
|
Required.Where(n => !File.Exists(Path.Combine(CaptureFile.Dir, n))).ShouldBeEmpty();
|
|
|
|
[Theory]
|
|
[MemberData(nameof(AllCaptures))]
|
|
public void Every_frame_length_field_matches_its_data_length(string name)
|
|
{
|
|
var frames = CaptureFile.Load(name);
|
|
|
|
frames.ShouldNotBeEmpty();
|
|
foreach (var f in frames)
|
|
{
|
|
f.Bytes.Length.ShouldBeGreaterThanOrEqualTo(24, name);
|
|
BitConverter.ToUInt16(f.Bytes, 2).ShouldBe((ushort)(f.Bytes.Length - 24), name);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(AllCaptures))]
|
|
public void Frames_alternate_request_then_reply(string name)
|
|
{
|
|
var frames = CaptureFile.Load(name);
|
|
|
|
frames.Count.ShouldBe(frames.Count / 2 * 2, name);
|
|
for (var i = 0; i < frames.Count; i++) frames[i].FromClient.ShouldBe(i % 2 == 0, $"{name} frame {i}");
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(AllCaptures))]
|
|
public void Every_CPF_item_walk_ends_exactly_at_the_frame_end(string name)
|
|
{
|
|
foreach (var f in CaptureFile.Load(name).Where(f => BitConverter.ToUInt16(f.Bytes, 0) is 0x6F or 0x70))
|
|
{
|
|
var pos = 24 + 4 + 2;
|
|
var itemCount = BitConverter.ToUInt16(f.Bytes, pos);
|
|
pos += 2;
|
|
for (var i = 0; i < itemCount; i++) pos += 4 + BitConverter.ToUInt16(f.Bytes, pos + 2);
|
|
|
|
pos.ShouldBe(f.Bytes.Length, name);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(AllCaptures))]
|
|
public void Every_reply_service_is_its_request_service_with_the_reply_bit(string name)
|
|
{
|
|
var frames = CaptureFile.Load(name);
|
|
for (var i = 0; i + 1 < frames.Count; i += 2)
|
|
{
|
|
var request = CaptureFile.CipPayload(frames[i]);
|
|
var reply = CaptureFile.CipPayload(frames[i + 1]);
|
|
if (request.Length == 0) continue; // RegisterSession: no CIP
|
|
|
|
reply[0].ShouldBe((byte)(request[0] | 0x80), $"{name} pair {i / 2}");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void CipPayload_strips_the_connected_sequence_count()
|
|
{
|
|
// Connected Read Tag Fragmented of TestDINT: service 0x52, path size 5 words, symbol 0x91 len 8.
|
|
var request = CaptureFile.Load("logix-read-dint.hex")[0];
|
|
|
|
CaptureFile.CipPayload(request)[..4].ShouldBe(new byte[] { 0x52, 0x05, 0x91, 0x08 });
|
|
}
|
|
|
|
[Fact]
|
|
public void CipPayload_returns_the_unconnected_item_for_SendRRData()
|
|
{
|
|
// Large Forward Open request goes out in an unconnected data item (0xB2): service 0x5B.
|
|
var request = CaptureFile.Load("logix-forward-open.hex")[0];
|
|
|
|
CaptureFile.CipPayload(request)[0].ShouldBe((byte)0x5B);
|
|
}
|
|
|
|
[Fact]
|
|
public void CipPayload_is_empty_for_RegisterSession() =>
|
|
CaptureFile.CipPayload(CaptureFile.Load("logix-register-session.hex")[0]).ShouldBeEmpty();
|
|
}
|