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,175 @@
|
||||
using MxGateway.Contracts.Proto.Galaxy;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="GalaxyDriver"/>'s <c>IWritable</c> wiring. Verifies the
|
||||
/// SecurityClassification per-tag map gets populated during Discovery and routes the
|
||||
/// subsequent WriteAsync calls to the right gateway command (Write vs WriteSecured).
|
||||
/// The actual Write / WriteSecured invocation is tested separately at the
|
||||
/// <see cref="GatewayGalaxyDataWriter"/> level — this test class focuses on the
|
||||
/// driver-side wiring.
|
||||
/// </summary>
|
||||
public sealed class GalaxyDriverWriteTests
|
||||
{
|
||||
private static GalaxyDriverOptions Opts() => new(
|
||||
new GalaxyGatewayOptions("https://mxgw.test:5001", "key"),
|
||||
new GalaxyMxAccessOptions("OtOpcUa-A"),
|
||||
new GalaxyRepositoryOptions(),
|
||||
new GalaxyReconnectOptions());
|
||||
|
||||
private sealed class FakeHierarchySource(IReadOnlyList<GalaxyObject> objects) : IGalaxyHierarchySource
|
||||
{
|
||||
public Task<IReadOnlyList<GalaxyObject>> GetHierarchyAsync(CancellationToken cancellationToken)
|
||||
=> Task.FromResult(objects);
|
||||
}
|
||||
|
||||
private sealed class FakeBuilder : IAddressSpaceBuilder
|
||||
{
|
||||
public List<DriverAttributeInfo> Variables { get; } = [];
|
||||
|
||||
public IAddressSpaceBuilder Folder(string browseName, string displayName) => this;
|
||||
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
|
||||
{
|
||||
Variables.Add(attributeInfo);
|
||||
return new FakeHandle(attributeInfo.FullName);
|
||||
}
|
||||
public void AddProperty(string browseName, DriverDataType dataType, object? value) { }
|
||||
|
||||
private sealed class FakeHandle(string fullRef) : IVariableHandle
|
||||
{
|
||||
public string FullReference { get; } = fullRef;
|
||||
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NoopSink();
|
||||
private sealed class NoopSink : IAlarmConditionSink { public void OnTransition(AlarmEventArgs args) { } }
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeWriter : IGalaxyDataWriter
|
||||
{
|
||||
public List<(string FullRef, object? Value, SecurityClassification Resolved)> Calls { get; } = [];
|
||||
|
||||
public Task<IReadOnlyList<WriteResult>> WriteAsync(
|
||||
IReadOnlyList<WriteRequest> writes,
|
||||
Func<string, SecurityClassification> securityResolver,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var results = new WriteResult[writes.Count];
|
||||
for (var i = 0; i < writes.Count; i++)
|
||||
{
|
||||
Calls.Add((writes[i].FullReference, writes[i].Value, securityResolver(writes[i].FullReference)));
|
||||
results[i] = new WriteResult(StatusCodeMap.Good);
|
||||
}
|
||||
return Task.FromResult<IReadOnlyList<WriteResult>>(results);
|
||||
}
|
||||
}
|
||||
|
||||
private static GalaxyAttribute Attr(string name, int sec)
|
||||
=> new() { AttributeName = name, MxDataType = 2 /*Float32*/, SecurityClassification = sec };
|
||||
|
||||
private static GalaxyObject Obj(string tag, params GalaxyAttribute[] attrs)
|
||||
{
|
||||
var o = new GalaxyObject { TagName = tag, ContainedName = tag };
|
||||
o.Attributes.AddRange(attrs);
|
||||
return o;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_RoutesThroughInjectedWriter_AndPropagatesValues()
|
||||
{
|
||||
var src = new FakeHierarchySource([
|
||||
Obj("Tank1_Level", Attr("PV", sec: 0 /*FreeAccess*/), Attr("SP", sec: 1 /*Operate*/)),
|
||||
]);
|
||||
var writer = new FakeWriter();
|
||||
var driver = new GalaxyDriver(
|
||||
"g", Opts(), hierarchySource: src, dataReader: null, dataWriter: writer);
|
||||
|
||||
var builder = new FakeBuilder();
|
||||
await driver.DiscoverAsync(builder, CancellationToken.None);
|
||||
|
||||
await driver.WriteAsync([
|
||||
new WriteRequest("Tank1_Level.PV", 42.0),
|
||||
new WriteRequest("Tank1_Level.SP", 50.0),
|
||||
], CancellationToken.None);
|
||||
|
||||
writer.Calls.Count.ShouldBe(2);
|
||||
writer.Calls[0].Resolved.ShouldBe(SecurityClassification.FreeAccess);
|
||||
writer.Calls[1].Resolved.ShouldBe(SecurityClassification.Operate);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, SecurityClassification.FreeAccess)]
|
||||
[InlineData(1, SecurityClassification.Operate)]
|
||||
[InlineData(2, SecurityClassification.SecuredWrite)]
|
||||
[InlineData(3, SecurityClassification.VerifiedWrite)]
|
||||
[InlineData(4, SecurityClassification.Tune)]
|
||||
[InlineData(5, SecurityClassification.Configure)]
|
||||
[InlineData(6, SecurityClassification.ViewOnly)]
|
||||
public async Task WriteAsync_ResolvesEverySecurityClassification_FromDiscovery(int mxSec, SecurityClassification expected)
|
||||
{
|
||||
var src = new FakeHierarchySource([
|
||||
Obj("Tank", Attr("PV", sec: mxSec)),
|
||||
]);
|
||||
var writer = new FakeWriter();
|
||||
var driver = new GalaxyDriver(
|
||||
"g", Opts(), hierarchySource: src, dataReader: null, dataWriter: writer);
|
||||
|
||||
await driver.DiscoverAsync(new FakeBuilder(), CancellationToken.None);
|
||||
await driver.WriteAsync([new WriteRequest("Tank.PV", 1.0)], CancellationToken.None);
|
||||
|
||||
writer.Calls[0].Resolved.ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_UnknownTag_ResolvesToFreeAccess_DefaultsToWrite()
|
||||
{
|
||||
var writer = new FakeWriter();
|
||||
var driver = new GalaxyDriver(
|
||||
"g", Opts(), hierarchySource: null, dataReader: null, dataWriter: writer);
|
||||
|
||||
// No DiscoverAsync call → classification map is empty → resolver returns FreeAccess
|
||||
// for any tag the gateway might attempt. WriteAsync must not throw on unknown tags.
|
||||
await driver.WriteAsync([new WriteRequest("Random.Tag", 1.0)], CancellationToken.None);
|
||||
|
||||
writer.Calls[0].Resolved.ShouldBe(SecurityClassification.FreeAccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_EmptyRequest_ReturnsEmpty_WithoutCallingWriter()
|
||||
{
|
||||
var writer = new FakeWriter();
|
||||
var driver = new GalaxyDriver(
|
||||
"g", Opts(), hierarchySource: null, dataReader: null, dataWriter: writer);
|
||||
|
||||
var result = await driver.WriteAsync([], CancellationToken.None);
|
||||
|
||||
result.ShouldBeEmpty();
|
||||
writer.Calls.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_NoWriter_Throws_PointingAtPR44()
|
||||
{
|
||||
var driver = new GalaxyDriver("g", Opts());
|
||||
|
||||
var ex = await Should.ThrowAsync<NotSupportedException>(() =>
|
||||
driver.WriteAsync([new WriteRequest("x", 1)], CancellationToken.None));
|
||||
ex.Message.ShouldContain("PR 4.4");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_AfterDispose_Throws()
|
||||
{
|
||||
var writer = new FakeWriter();
|
||||
var driver = new GalaxyDriver(
|
||||
"g", Opts(), hierarchySource: null, dataReader: null, dataWriter: writer);
|
||||
driver.Dispose();
|
||||
await Should.ThrowAsync<ObjectDisposedException>(() =>
|
||||
driver.WriteAsync([new WriteRequest("x", 1)], CancellationToken.None));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user