chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
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() { }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user