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>
123 lines
5.0 KiB
C#
123 lines
5.0 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
|
using ZB.MOM.WW.OtOpcUa.Server.Phase7;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Server.Tests.Phase7;
|
|
|
|
/// <summary>
|
|
/// PR B.4 — pins the precedence order Phase7Composer uses to pick an
|
|
/// <see cref="IAlarmHistorianWriter"/>:
|
|
/// driver-provided > DI-registered > none. Driver wins so a future
|
|
/// GalaxyDriver-as-IAlarmHistorianWriter takes the write path directly,
|
|
/// preserving the v1 invariant where a driver that natively owns the
|
|
/// historian client doesn't bounce through the sidecar IPC.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class Phase7ComposerWriterSelectionTests
|
|
{
|
|
[Fact]
|
|
public async Task No_driver_no_injected_writer_returns_null()
|
|
{
|
|
await using var host = new DriverHost();
|
|
|
|
var writer = Phase7Composer.SelectAlarmHistorianWriter(host, injectedWriter: null, out var source);
|
|
|
|
writer.ShouldBeNull();
|
|
source.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Injected_writer_only_is_selected()
|
|
{
|
|
await using var host = new DriverHost();
|
|
var injected = new RecordingWriter("from-di");
|
|
|
|
var writer = Phase7Composer.SelectAlarmHistorianWriter(host, injected, out var source);
|
|
|
|
writer.ShouldBeSameAs(injected);
|
|
source.ShouldStartWith("di:");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Driver_writer_wins_over_injected()
|
|
{
|
|
await using var host = new DriverHost();
|
|
var driver = new FakeDriverWithWriter("drv-1", "drv-out");
|
|
await host.RegisterAsync(driver, driverConfigJson: "{}", CancellationToken.None);
|
|
|
|
var injected = new RecordingWriter("from-di");
|
|
var writer = Phase7Composer.SelectAlarmHistorianWriter(host, injected, out var source);
|
|
|
|
writer.ShouldBeSameAs(driver);
|
|
source.ShouldBe("driver:drv-1");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task First_driver_implementing_writer_wins()
|
|
{
|
|
await using var host = new DriverHost();
|
|
var driverNoWriter = new FakeDriverWithoutWriter("drv-1");
|
|
var driverWithWriter = new FakeDriverWithWriter("drv-2", "drv-out");
|
|
|
|
await host.RegisterAsync(driverNoWriter, "{}", CancellationToken.None);
|
|
await host.RegisterAsync(driverWithWriter, "{}", CancellationToken.None);
|
|
|
|
var writer = Phase7Composer.SelectAlarmHistorianWriter(host, injectedWriter: null, out var source);
|
|
|
|
writer.ShouldBeSameAs(driverWithWriter);
|
|
source.ShouldBe("driver:drv-2");
|
|
}
|
|
|
|
private sealed class RecordingWriter : IAlarmHistorianWriter
|
|
{
|
|
public string Tag { get; }
|
|
public RecordingWriter(string tag) { Tag = tag; }
|
|
|
|
public Task<IReadOnlyList<HistorianWriteOutcome>> WriteBatchAsync(
|
|
IReadOnlyList<AlarmHistorianEvent> batch, CancellationToken cancellationToken)
|
|
{
|
|
var outcomes = new HistorianWriteOutcome[batch.Count];
|
|
for (var i = 0; i < outcomes.Length; i++) outcomes[i] = HistorianWriteOutcome.Ack;
|
|
return Task.FromResult<IReadOnlyList<HistorianWriteOutcome>>(outcomes);
|
|
}
|
|
}
|
|
|
|
private sealed class FakeDriverWithoutWriter : IDriver
|
|
{
|
|
public FakeDriverWithoutWriter(string id) { DriverInstanceId = id; }
|
|
public string DriverInstanceId { get; }
|
|
public string DriverType => "FakeNoWriter";
|
|
public Task InitializeAsync(string c, CancellationToken ct) => Task.CompletedTask;
|
|
public Task ReinitializeAsync(string c, CancellationToken ct) => Task.CompletedTask;
|
|
public Task ShutdownAsync(CancellationToken ct) => Task.CompletedTask;
|
|
public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, null);
|
|
public long GetMemoryFootprint() => 0;
|
|
public Task FlushOptionalCachesAsync(CancellationToken ct) => Task.CompletedTask;
|
|
}
|
|
|
|
private sealed class FakeDriverWithWriter : IDriver, IAlarmHistorianWriter
|
|
{
|
|
private readonly RecordingWriter _writer;
|
|
public FakeDriverWithWriter(string id, string tag)
|
|
{
|
|
DriverInstanceId = id;
|
|
_writer = new RecordingWriter(tag);
|
|
}
|
|
public string DriverInstanceId { get; }
|
|
public string DriverType => "FakeWithWriter";
|
|
public Task InitializeAsync(string c, CancellationToken ct) => Task.CompletedTask;
|
|
public Task ReinitializeAsync(string c, CancellationToken ct) => Task.CompletedTask;
|
|
public Task ShutdownAsync(CancellationToken ct) => Task.CompletedTask;
|
|
public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, null);
|
|
public long GetMemoryFootprint() => 0;
|
|
public Task FlushOptionalCachesAsync(CancellationToken ct) => Task.CompletedTask;
|
|
|
|
public Task<IReadOnlyList<HistorianWriteOutcome>> WriteBatchAsync(
|
|
IReadOnlyList<AlarmHistorianEvent> batch, CancellationToken cancellationToken)
|
|
=> _writer.WriteBatchAsync(batch, cancellationToken);
|
|
}
|
|
}
|