222 lines
8.0 KiB
C#
222 lines
8.0 KiB
C#
using System.Buffers.Binary;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class AbCipFetchUdtShapeTests
|
|
{
|
|
private sealed class FakeTemplateReader : IAbCipTemplateReader
|
|
{
|
|
public byte[] Response { get; set; } = [];
|
|
public int ReadCount { get; private set; }
|
|
public bool Disposed { get; private set; }
|
|
public uint LastTemplateId { get; private set; }
|
|
|
|
public Task<byte[]> ReadAsync(AbCipTagCreateParams deviceParams, uint templateInstanceId, CancellationToken ct)
|
|
{
|
|
ReadCount++;
|
|
LastTemplateId = templateInstanceId;
|
|
return Task.FromResult(Response);
|
|
}
|
|
|
|
public void Dispose() => Disposed = true;
|
|
}
|
|
|
|
private sealed class FakeTemplateReaderFactory : IAbCipTemplateReaderFactory
|
|
{
|
|
public List<IAbCipTemplateReader> Readers { get; } = new();
|
|
public Func<IAbCipTemplateReader>? Customise { get; set; }
|
|
|
|
public IAbCipTemplateReader Create()
|
|
{
|
|
var r = Customise?.Invoke() ?? new FakeTemplateReader();
|
|
Readers.Add(r);
|
|
return r;
|
|
}
|
|
}
|
|
|
|
private static byte[] BuildSimpleTemplate(string name, uint instanceSize, params (string n, ushort info, ushort arr, uint off)[] members)
|
|
{
|
|
var headerSize = 12;
|
|
var blockSize = 8;
|
|
var strings = new MemoryStream();
|
|
void Add(string s) { var b = Encoding.ASCII.GetBytes(s + ";\0"); strings.Write(b, 0, b.Length); }
|
|
Add(name);
|
|
foreach (var m in members) Add(m.n);
|
|
var stringsArr = strings.ToArray();
|
|
|
|
var buf = new byte[headerSize + blockSize * members.Length + stringsArr.Length];
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0), (ushort)members.Length);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(2), 0x1234);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(4), instanceSize);
|
|
for (var i = 0; i < members.Length; i++)
|
|
{
|
|
var o = headerSize + i * blockSize;
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(o), members[i].info);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(o + 2), members[i].arr);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(o + 4), members[i].off);
|
|
}
|
|
Buffer.BlockCopy(stringsArr, 0, buf, headerSize + blockSize * members.Length, stringsArr.Length);
|
|
return buf;
|
|
}
|
|
|
|
private static Task<AbCipUdtShape?> InvokeFetch(AbCipDriver drv, string deviceHostAddress, uint templateId)
|
|
{
|
|
var mi = typeof(AbCipDriver).GetMethod("FetchUdtShapeAsync",
|
|
BindingFlags.NonPublic | BindingFlags.Instance)!;
|
|
return (Task<AbCipUdtShape?>)mi.Invoke(drv, [deviceHostAddress, templateId, CancellationToken.None])!;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FetchUdtShapeAsync_decodes_blob_and_caches_result()
|
|
{
|
|
var factory = new FakeTemplateReaderFactory
|
|
{
|
|
Customise = () => new FakeTemplateReader
|
|
{
|
|
Response = BuildSimpleTemplate("MotorUdt", 8,
|
|
("Speed", 0xC4, 0, 0),
|
|
("Enabled", 0xC1, 0, 4)),
|
|
},
|
|
};
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
}, "drv-1", templateReaderFactory: factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var shape = await InvokeFetch(drv, "ab://10.0.0.5/1,0", 42);
|
|
|
|
shape.ShouldNotBeNull();
|
|
shape.TypeName.ShouldBe("MotorUdt");
|
|
shape.Members.Count.ShouldBe(2);
|
|
|
|
// Second fetch must hit the cache — no second reader created.
|
|
_ = await InvokeFetch(drv, "ab://10.0.0.5/1,0", 42);
|
|
factory.Readers.Count.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FetchUdtShapeAsync_different_templateIds_each_fetch()
|
|
{
|
|
var callCount = 0;
|
|
var factory = new FakeTemplateReaderFactory
|
|
{
|
|
Customise = () =>
|
|
{
|
|
callCount++;
|
|
var name = callCount == 1 ? "UdtA" : "UdtB";
|
|
return new FakeTemplateReader
|
|
{
|
|
Response = BuildSimpleTemplate(name, 4, ("X", 0xC4, 0, 0)),
|
|
};
|
|
},
|
|
};
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
}, "drv-1", templateReaderFactory: factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var a = await InvokeFetch(drv, "ab://10.0.0.5/1,0", 1);
|
|
var b = await InvokeFetch(drv, "ab://10.0.0.5/1,0", 2);
|
|
|
|
a!.TypeName.ShouldBe("UdtA");
|
|
b!.TypeName.ShouldBe("UdtB");
|
|
factory.Readers.Count.ShouldBe(2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FetchUdtShapeAsync_unknown_device_returns_null()
|
|
{
|
|
var factory = new FakeTemplateReaderFactory();
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
}, "drv-1", templateReaderFactory: factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var shape = await InvokeFetch(drv, "ab://10.0.0.99/1,0", 1);
|
|
shape.ShouldBeNull();
|
|
factory.Readers.ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FetchUdtShapeAsync_decode_failure_returns_null_and_does_not_cache()
|
|
{
|
|
var factory = new FakeTemplateReaderFactory
|
|
{
|
|
Customise = () => new FakeTemplateReader { Response = [0x00, 0x00] }, // too short
|
|
};
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
}, "drv-1", templateReaderFactory: factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var shape = await InvokeFetch(drv, "ab://10.0.0.5/1,0", 1);
|
|
shape.ShouldBeNull();
|
|
|
|
// Next call retries (not cached as a failure).
|
|
var shape2 = await InvokeFetch(drv, "ab://10.0.0.5/1,0", 1);
|
|
shape2.ShouldBeNull();
|
|
factory.Readers.Count.ShouldBe(2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FetchUdtShapeAsync_reader_exception_returns_null()
|
|
{
|
|
var factory = new FakeTemplateReaderFactory
|
|
{
|
|
Customise = () => new ThrowingTemplateReader(),
|
|
};
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
}, "drv-1", templateReaderFactory: factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var shape = await InvokeFetch(drv, "ab://10.0.0.5/1,0", 1);
|
|
shape.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FlushOptionalCachesAsync_empties_template_cache()
|
|
{
|
|
var factory = new FakeTemplateReaderFactory
|
|
{
|
|
Customise = () => new FakeTemplateReader
|
|
{
|
|
Response = BuildSimpleTemplate("U", 4, ("X", 0xC4, 0, 0)),
|
|
},
|
|
};
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
}, "drv-1", templateReaderFactory: factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
_ = await InvokeFetch(drv, "ab://10.0.0.5/1,0", 99);
|
|
drv.TemplateCache.Count.ShouldBe(1);
|
|
|
|
await drv.FlushOptionalCachesAsync(CancellationToken.None);
|
|
drv.TemplateCache.Count.ShouldBe(0);
|
|
|
|
// Next fetch hits the network again.
|
|
_ = await InvokeFetch(drv, "ab://10.0.0.5/1,0", 99);
|
|
factory.Readers.Count.ShouldBe(2);
|
|
}
|
|
|
|
private sealed class ThrowingTemplateReader : IAbCipTemplateReader
|
|
{
|
|
public Task<byte[]> ReadAsync(AbCipTagCreateParams p, uint id, CancellationToken ct) =>
|
|
throw new InvalidOperationException("fake read failure");
|
|
public void Dispose() { }
|
|
}
|
|
}
|