feat(ablegacy): native PCCC tag runtime over the managed transport
This commit is contained in:
+424
@@ -0,0 +1,424 @@
|
||||
using System.Buffers.Binary;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Captures;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Fakes;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class NativeAbLegacyTagRuntimeTests
|
||||
{
|
||||
// The requestor identity libplctag used in the golden captures, so our request bytes can be compared
|
||||
// to the captured ones apart from the TNS.
|
||||
private const ushort Vendor = 0xF33D;
|
||||
private const uint Serial = 0x21504345;
|
||||
|
||||
private sealed class Harness : IAsyncDisposable
|
||||
{
|
||||
public FakeCipChannel Channel { get; } = new();
|
||||
public RequestScheduler Scheduler { get; }
|
||||
public NativeAbLegacyTagRuntime Runtime { get; }
|
||||
|
||||
public Harness(string plc, string tag, int count = 1)
|
||||
{
|
||||
Scheduler = new RequestScheduler(Channel, allowPacking: false);
|
||||
Runtime = new NativeAbLegacyTagRuntime(
|
||||
new AbLegacyTagCreateParams("10.0.0.1", 44818, "1,0", plc, tag, TimeSpan.FromSeconds(2), count),
|
||||
Scheduler, Vendor, Serial);
|
||||
}
|
||||
|
||||
/// <summary>Answers every request with <paramref name="data"/>, echoing the request's requestor id and TNS.</summary>
|
||||
public Harness Replies(byte[] data) => Responder((_, _) => (0x00, null, data));
|
||||
|
||||
public Harness Responder(Func<byte[], int, (byte Sts, byte? Ext, byte[] Data)> f)
|
||||
{
|
||||
var n = 0;
|
||||
Channel.Respond = req =>
|
||||
{
|
||||
var (sts, ext, data) = f(req, n++);
|
||||
return Task.FromResult(Echo(req, sts, ext, data));
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
Runtime.Dispose();
|
||||
await Scheduler.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// Execute PCCC request: 4B 02 20 67 24 01 | requestor id (7) | cmd 0F, sts 00, tns(2), fnc, ...
|
||||
private static ushort TnsOf(byte[] req) => BinaryPrimitives.ReadUInt16LittleEndian(req.AsSpan(15));
|
||||
private static byte FncOf(byte[] req) => req[17];
|
||||
|
||||
private static byte[] Echo(byte[] req, byte sts, byte? ext, byte[] data)
|
||||
{
|
||||
var body = new List<byte>(req[6..13]) { 0x4F, sts, req[15], req[16] };
|
||||
if (ext is { } e) body.Add(e);
|
||||
body.AddRange(data);
|
||||
return FakeCipChannel.Reply(0x4B, 0x00, [.. body]);
|
||||
}
|
||||
|
||||
private static byte[] Words(params ushort[] w)
|
||||
{
|
||||
var b = new byte[w.Length * 2];
|
||||
for (var i = 0; i < w.Length; i++) BinaryPrimitives.WriteUInt16LittleEndian(b.AsSpan(i * 2), w[i]);
|
||||
return b;
|
||||
}
|
||||
|
||||
/// <summary>The PCCC data bytes of a golden capture's first reply (after cmd, sts, tns).</summary>
|
||||
private static byte[] GoldenData(string capture)
|
||||
{
|
||||
var f = CaptureFile.Load(capture).First(x => !x.FromClient && CaptureFile.CipPayload(x).Length > 0);
|
||||
return CaptureFile.CipPayload(f)[15..];
|
||||
}
|
||||
|
||||
private static void ShouldMatchCaptureExceptTns(string capture, byte[] sent)
|
||||
{
|
||||
var golden = CaptureFile.CipPayload(CaptureFile.Load(capture).First(x => x.FromClient && CaptureFile.CipPayload(x).Length > 0));
|
||||
sent.Length.ShouldBe(golden.Length);
|
||||
for (var i = 0; i < golden.Length; i++)
|
||||
if (i is not (15 or 16)) sent[i].ShouldBe(golden[i], $"byte {i}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task N7_0_read_decodes_the_golden_slc500_value_and_sends_the_captured_request()
|
||||
{
|
||||
await using var h = new Harness("slc500", "N7:0").Replies(GoldenData("slc500-read-n7.hex"));
|
||||
await h.Runtime.ReadAsync(CancellationToken.None);
|
||||
h.Runtime.DecodeValue(AbLegacyDataType.Int, null).ShouldBe(4321);
|
||||
h.Runtime.GetStatus().ShouldBe(0);
|
||||
ShouldMatchCaptureExceptTns("slc500-read-n7.hex", h.Channel.Sent.Single());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task F8_0_read_decodes_the_golden_float()
|
||||
{
|
||||
await using var h = new Harness("slc500", "F8:0").Replies(GoldenData("slc500-read-f8.hex"));
|
||||
await h.Runtime.ReadAsync(CancellationToken.None);
|
||||
h.Runtime.DecodeValue(AbLegacyDataType.Float, null).ShouldBe(3.5f);
|
||||
ShouldMatchCaptureExceptTns("slc500-read-f8.hex", h.Channel.Sent.Single());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MicroLogix_N7_0_read_decodes_the_golden_value()
|
||||
{
|
||||
await using var h = new Harness("micrologix", "N7:0").Replies(GoldenData("micrologix-read-n7.hex"));
|
||||
await h.Runtime.ReadAsync(CancellationToken.None);
|
||||
h.Runtime.DecodeValue(AbLegacyDataType.Int, null).ShouldBe(2468);
|
||||
ShouldMatchCaptureExceptTns("micrologix-read-n7.hex", h.Channel.Sent.Single());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Plc5_N7_0_read_uses_the_range_read_and_decodes_the_golden_value()
|
||||
{
|
||||
await using var h = new Harness("plc5", "N7:0").Replies(GoldenData("plc5-read-n7.hex"));
|
||||
await h.Runtime.ReadAsync(CancellationToken.None);
|
||||
h.Runtime.DecodeValue(AbLegacyDataType.Int, null).ShouldBe(1357);
|
||||
FncOf(h.Channel.Sent.Single()).ShouldBe((byte)0x01);
|
||||
ShouldMatchCaptureExceptTns("plc5-read-n7.hex", h.Channel.Sent.Single());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task B3_0_bit_5_decodes_from_the_little_endian_word()
|
||||
{
|
||||
await using var h = new Harness("slc500", "B3:0/5").Replies(Words(0x0020));
|
||||
await h.Runtime.ReadAsync(CancellationToken.None);
|
||||
h.Runtime.DecodeValue(AbLegacyDataType.Bit, 5).ShouldBe(true);
|
||||
h.Runtime.DecodeValue(AbLegacyDataType.Bit, 4).ShouldBe(false);
|
||||
h.Runtime.DecodeValue(AbLegacyDataType.Bit, null).ShouldBe(false); // no index: bit 0 of the word
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_bit_above_15_in_an_L_file_decodes_from_the_32_bit_word()
|
||||
{
|
||||
await using var h = new Harness("slc500", "L9:0/20").Replies([0x00, 0x00, 0x10, 0x00]);
|
||||
await h.Runtime.ReadAsync(CancellationToken.None);
|
||||
h.Runtime.DecodeValue(AbLegacyDataType.Bit, 20).ShouldBe(true);
|
||||
h.Runtime.DecodeValue(AbLegacyDataType.Long, null).ShouldBe(0x100000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task An_N7_array_of_5_reads_ten_bytes_and_decodes_each_element()
|
||||
{
|
||||
await using var h = new Harness("slc500", "N7:0", count: 5).Replies(Words(10, 20, 30, 0xFFFF, 50));
|
||||
await h.Runtime.ReadAsync(CancellationToken.None);
|
||||
var req = h.Channel.Sent.Single();
|
||||
req[18].ShouldBe((byte)10); // byte count
|
||||
h.Runtime.DecodeArray(AbLegacyDataType.Int, 5).ShouldBe(new short[] { 10, 20, 30, -1, 50 });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_B_file_array_decodes_bit_packed()
|
||||
{
|
||||
await using var h = new Harness("slc500", "B3:0", count: 2).Replies(Words(0x0005, 0x0000));
|
||||
await h.Runtime.ReadAsync(CancellationToken.None);
|
||||
var bits = (bool[])h.Runtime.DecodeArray(AbLegacyDataType.Bit, 4)!;
|
||||
bits.ShouldBe(new[] { true, false, true, false });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task T4_0_ACC_addresses_word_2_of_the_timer_and_reads_two_bytes()
|
||||
{
|
||||
await using var h = new Harness("slc500", "T4:0.ACC").Replies(Words(500));
|
||||
await h.Runtime.ReadAsync(CancellationToken.None);
|
||||
var req = h.Channel.Sent.Single();
|
||||
// 0F 00 tns(2) A2 byteCount file type element sub (file 4, type T = 0x86, element 0, sub-element 2)
|
||||
req[13..].Where((_, i) => i is 0 or 1 or 4 or 5 or 6 or 7 or 8 or 9).ToArray()
|
||||
.ShouldBe(new byte[] { 0x0F, 0x00, 0xA2, 0x02, 0x04, 0x86, 0x00, 0x02 });
|
||||
h.Runtime.DecodeValue(AbLegacyDataType.Int, null).ShouldBe(500);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_whole_timer_reads_six_bytes_and_decodes_the_first_four_as_Int32()
|
||||
{
|
||||
await using var h = new Harness("slc500", "T4:1").Replies([0x01, 0x00, 0x02, 0x00, 0x03, 0x00]);
|
||||
await h.Runtime.ReadAsync(CancellationToken.None);
|
||||
h.Channel.Sent.Single()[18].ShouldBe((byte)6);
|
||||
h.Runtime.DecodeValue(AbLegacyDataType.TimerElement, null).ShouldBe(0x00020001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_status_bit_sub_element_decodes_its_bit_from_word_0()
|
||||
{
|
||||
await using var h = new Harness("slc500", "T4:0.DN").Replies(Words(1 << 13));
|
||||
await h.Runtime.ReadAsync(CancellationToken.None);
|
||||
h.Channel.Sent.Single()[18].ShouldBe((byte)2);
|
||||
h.Runtime.DecodeValue(AbLegacyDataType.Bit, null).ShouldBe(true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_string_decodes_with_each_byte_pair_swapped()
|
||||
{
|
||||
var data = new byte[84];
|
||||
data[0] = 5; // LEN
|
||||
// "hello" stored pair-swapped: eh ll \0o
|
||||
data[2] = (byte)'e'; data[3] = (byte)'h'; data[4] = (byte)'l'; data[5] = (byte)'l'; data[6] = 0; data[7] = (byte)'o';
|
||||
await using var h = new Harness("slc500", "ST9:0").Replies(data);
|
||||
await h.Runtime.ReadAsync(CancellationToken.None);
|
||||
h.Channel.Sent.Single()[18].ShouldBe((byte)84);
|
||||
h.Runtime.DecodeValue(AbLegacyDataType.String, null).ShouldBe("hello");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_string_write_encodes_len_and_swapped_pairs()
|
||||
{
|
||||
await using var h = new Harness("slc500", "ST9:0").Replies([]);
|
||||
h.Runtime.EncodeValue(AbLegacyDataType.String, null, "hello");
|
||||
await h.Runtime.WriteAsync(CancellationToken.None);
|
||||
var data = h.Channel.Sent.Single()[23..];
|
||||
data.Length.ShouldBe(84);
|
||||
data[0].ShouldBe((byte)5);
|
||||
data[1].ShouldBe((byte)0);
|
||||
data[2..8].ShouldBe(new[] { (byte)'e', (byte)'h', (byte)'l', (byte)'l', (byte)0, (byte)'o' });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task STS_0x20_on_initialize_is_NotFound()
|
||||
{
|
||||
await using var h = new Harness("slc500", "N99:0").Responder((_, _) => (0x20, null, []));
|
||||
var ex = await Should.ThrowAsync<AbTransportException>(() => h.Runtime.InitializeAsync(CancellationToken.None));
|
||||
ex.Status.ShouldBe(AbStatus.NotFound);
|
||||
h.Runtime.GetStatus().ShouldBe((int)AbStatus.NotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Initialize_performs_exactly_one_read()
|
||||
{
|
||||
await using var h = new Harness("slc500", "N7:0").Replies(Words(1));
|
||||
await h.Runtime.InitializeAsync(CancellationToken.None);
|
||||
h.Channel.SentCount.ShouldBe(1);
|
||||
h.Runtime.DecodeValue(AbLegacyDataType.Int, null).ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Writing_N7_0_equals_42_sends_the_expected_PCCC_bytes()
|
||||
{
|
||||
await using var h = new Harness("slc500", "N7:0").Replies([]);
|
||||
h.Runtime.EncodeValue(AbLegacyDataType.Int, null, 42);
|
||||
await h.Runtime.WriteAsync(CancellationToken.None);
|
||||
var req = h.Channel.Sent.Single();
|
||||
req[..6].ShouldBe(new byte[] { 0x4B, 0x02, 0x20, 0x67, 0x24, 0x01 });
|
||||
req[6..13].ShouldBe(new byte[] { 0x07, 0x3D, 0xF3, 0x45, 0x43, 0x50, 0x21 });
|
||||
var tns = TnsOf(req);
|
||||
req[13..].ShouldBe(new byte[] { 0x0F, 0x00, (byte)tns, (byte)(tns >> 8), 0xAA, 0x02, 0x07, 0x89, 0x00, 0x00, 0x2A, 0x00 });
|
||||
h.Runtime.GetStatus().ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_TNS_increments_per_request()
|
||||
{
|
||||
await using var h = new Harness("slc500", "N7:0").Replies(Words(1));
|
||||
await h.Runtime.ReadAsync(CancellationToken.None);
|
||||
await h.Runtime.ReadAsync(CancellationToken.None);
|
||||
((ushort)(TnsOf(h.Channel.Sent[1]) - TnsOf(h.Channel.Sent[0]))).ShouldBe((ushort)1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_256_word_read_is_three_chunks_of_118_118_20_words()
|
||||
{
|
||||
await using var h = new Harness("slc500", "N7:0", count: 256).Responder((req, _) =>
|
||||
{
|
||||
// 0F 00 tns A2 byteCount file type element sub (element < 255 or 0xFF-escaped)
|
||||
var bytes = req[18];
|
||||
var elem = req[21] == 0xFF ? BinaryPrimitives.ReadUInt16LittleEndian(req.AsSpan(22)) : req[21];
|
||||
var words = Enumerable.Range(elem, bytes / 2).Select(i => (ushort)i).ToArray();
|
||||
return (0x00, null, Words(words));
|
||||
});
|
||||
await h.Runtime.ReadAsync(CancellationToken.None);
|
||||
h.Channel.Sent.Select(r => (int)r[18]).ShouldBe(new[] { 236, 236, 40 });
|
||||
var arr = (short[])h.Runtime.DecodeArray(AbLegacyDataType.Int, 256)!;
|
||||
arr.Length.ShouldBe(256);
|
||||
for (var i = 0; i < 256; i++) arr[i].ShouldBe((short)i);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_failure_in_a_later_read_chunk_fails_the_whole_read_and_keeps_the_previous_value()
|
||||
{
|
||||
await using var h = new Harness("slc500", "N7:0", count: 256).Responder((_, n) =>
|
||||
n == 1 ? (0x10, null, []) : (0x00, null, new byte[236]));
|
||||
var ex = await Should.ThrowAsync<AbTransportException>(() => h.Runtime.ReadAsync(CancellationToken.None));
|
||||
ex.Status.ShouldBe(AbStatus.Unsupported);
|
||||
h.Runtime.GetStatus().ShouldBe((int)AbStatus.Unsupported);
|
||||
h.Channel.SentCount.ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_130_word_write_is_two_chunks_of_118_and_12_words()
|
||||
{
|
||||
await using var h = new Harness("slc500", "N7:0", count: 130).Replies([]);
|
||||
h.Runtime.EncodeValue(AbLegacyDataType.Int, null, 7);
|
||||
await h.Runtime.WriteAsync(CancellationToken.None);
|
||||
h.Channel.Sent.Select(r => (int)r[18]).ShouldBe(new[] { 236, 24 });
|
||||
h.Channel.Sent[0][21].ShouldBe((byte)0); // element 0
|
||||
h.Channel.Sent[1][21].ShouldBe((byte)118); // element advances by the words already sent
|
||||
h.Channel.Sent[0][23].ShouldBe((byte)7);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_failed_second_write_chunk_is_reported_as_failed_not_Good()
|
||||
{
|
||||
await using var h = new Harness("slc500", "N7:0", count: 130).Responder((_, n) =>
|
||||
n == 1 ? (0x30, null, []) : (0x00, null, []));
|
||||
var ex = await Should.ThrowAsync<AbTransportException>(() => h.Runtime.WriteAsync(CancellationToken.None));
|
||||
ex.Status.ShouldBe(AbStatus.NotAllowed);
|
||||
h.Runtime.GetStatus().ShouldBe((int)AbStatus.NotAllowed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_PLC5_write_is_Unsupported_and_sends_nothing()
|
||||
{
|
||||
await using var h = new Harness("plc5", "N7:0").Replies([]);
|
||||
h.Runtime.EncodeValue(AbLegacyDataType.Int, null, 1);
|
||||
var ex = await Should.ThrowAsync<AbTransportException>(() => h.Runtime.WriteAsync(CancellationToken.None));
|
||||
ex.Status.ShouldBe(AbStatus.Unsupported);
|
||||
h.Channel.SentCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("F8:0")]
|
||||
[InlineData("B3:0")]
|
||||
[InlineData("T4:0.ACC")]
|
||||
public async Task A_PLC5_read_outside_N_files_is_Unsupported_and_sends_nothing(string tag)
|
||||
{
|
||||
await using var h = new Harness("plc5", tag).Replies([]);
|
||||
var ex = await Should.ThrowAsync<AbTransportException>(() => h.Runtime.ReadAsync(CancellationToken.None));
|
||||
ex.Status.ShouldBe(AbStatus.Unsupported);
|
||||
h.Channel.SentCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_reply_with_a_different_requestor_id_is_BadReply()
|
||||
{
|
||||
await using var h = new Harness("slc500", "N7:0");
|
||||
h.Channel.Respond = req =>
|
||||
{
|
||||
var echoed = Echo(req, 0, null, Words(1));
|
||||
echoed[4 + 1] ^= 0xFF; // corrupt the echoed requestor id
|
||||
return Task.FromResult(echoed);
|
||||
};
|
||||
var ex = await Should.ThrowAsync<AbTransportException>(() => h.Runtime.ReadAsync(CancellationToken.None));
|
||||
ex.Status.ShouldBe(AbStatus.BadReply);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_reply_with_a_stale_TNS_is_BadReply()
|
||||
{
|
||||
await using var h = new Harness("slc500", "N7:0");
|
||||
h.Channel.Respond = req =>
|
||||
{
|
||||
var echoed = Echo(req, 0, null, Words(1));
|
||||
echoed[4 + 7 + 2] ^= 0x01; // flip a TNS bit
|
||||
return Task.FromResult(echoed);
|
||||
};
|
||||
var ex = await Should.ThrowAsync<AbTransportException>(() => h.Runtime.ReadAsync(CancellationToken.None));
|
||||
ex.Status.ShouldBe(AbStatus.BadReply);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_short_data_reply_is_BadReply()
|
||||
{
|
||||
await using var h = new Harness("slc500", "N7:0", count: 5).Replies(Words(1, 2));
|
||||
var ex = await Should.ThrowAsync<AbTransportException>(() => h.Runtime.ReadAsync(CancellationToken.None));
|
||||
ex.Status.ShouldBe(AbStatus.BadReply);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Bit_with_an_index_cannot_be_encoded()
|
||||
{
|
||||
await using var h = new Harness("slc500", "B3:0/5");
|
||||
Should.Throw<NotSupportedException>(() => h.Runtime.EncodeValue(AbLegacyDataType.Bit, 5, true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task An_unparseable_address_fails_the_operation_as_Unsupported()
|
||||
{
|
||||
await using var h = new Harness("slc500", "not-an-address");
|
||||
var ex = await Should.ThrowAsync<AbTransportException>(() => h.Runtime.InitializeAsync(CancellationToken.None));
|
||||
ex.Status.ShouldBe(AbStatus.Unsupported);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("slc500", CipRouting.Direct)]
|
||||
[InlineData("micrologix", CipRouting.Direct)]
|
||||
[InlineData("plc5", CipRouting.Direct)]
|
||||
// Captures/README.md "Routing observed": logixpccc sends Unconnected Send (0x52) with route 01 00.
|
||||
[InlineData("logixpccc", CipRouting.UnconnectedSend)]
|
||||
public void Routing_follows_the_captures_per_family(string plc, CipRouting expected)
|
||||
{
|
||||
NativeAbLegacyTagRuntime.RoutingFor(plc).ShouldBe(expected);
|
||||
var p = new AbLegacyTagCreateParams("h", 44818, "1,0", plc, "N7:0", TimeSpan.FromSeconds(3));
|
||||
var o = NativeAbLegacyTagRuntime.OptionsFor(p);
|
||||
o.Routing.ShouldBe(expected);
|
||||
o.ConnectionSize.ShouldBe(504);
|
||||
o.AllowPacking.ShouldBeFalse();
|
||||
o.Timeout.ShouldBe(TimeSpan.FromSeconds(3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_device_key_defaults_the_port_to_44818()
|
||||
{
|
||||
var p = new AbLegacyTagCreateParams("plc1", 0, "1,0", "slc500", "N7:0", TimeSpan.FromSeconds(3));
|
||||
NativeAbLegacyTagRuntime.KeyFor(p).ShouldBe(new DeviceKey("plc1", 44818, "1,0", "slc500"));
|
||||
var q = p with { Port = 2222 };
|
||||
NativeAbLegacyTagRuntime.KeyFor(q).Port.ShouldBe(2222);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_factory_leases_the_pooled_connection_and_releases_it_on_dispose()
|
||||
{
|
||||
var pool = new DeviceSessionPool();
|
||||
var factory = new NativeAbLegacyTagFactory(pool);
|
||||
var p = new AbLegacyTagCreateParams("127.0.0.1", 1, "1,0", "slc500", "N7:0", TimeSpan.FromSeconds(1));
|
||||
var a = factory.Create(p);
|
||||
var b = factory.Create(p with { TagName = "N7:1" });
|
||||
a.ShouldBeOfType<NativeAbLegacyTagRuntime>();
|
||||
a.Dispose();
|
||||
b.Dispose();
|
||||
await Task.Yield();
|
||||
}
|
||||
}
|
||||
+8
@@ -21,6 +21,14 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.AbLegacy\ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.csproj"/>
|
||||
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport\ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Shared transport test fakes + golden captures (same sources as the Ab.Transport tests). -->
|
||||
<Compile Include="..\ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests\Fakes\FakeCipChannel.cs" Link="Fakes\FakeCipChannel.cs"/>
|
||||
<Compile Include="..\ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests\Captures\CaptureFile.cs" Link="Captures\CaptureFile.cs"/>
|
||||
<None Include="..\ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests\Captures\*.hex" Link="Captures\%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user