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:
Joseph Doherty
2026-05-17 01:55:28 -04:00
parent 69f02fed7f
commit a25593a9c6
1044 changed files with 365 additions and 343 deletions

View File

@@ -0,0 +1,134 @@
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
[Trait("Category", "Unit")]
public sealed class FocasAlarmProjectionTests
{
private const string Host = "focas://10.0.0.5:8193";
private static (FocasDriver drv, FakeFocasClientFactory factory) NewDriver(bool alarmsEnabled)
{
var factory = new FakeFocasClientFactory();
var drv = new FocasDriver(new FocasDriverOptions
{
Devices = [new FocasDeviceOptions(Host)],
Tags = [],
Probe = new FocasProbeOptions { Enabled = false },
AlarmProjection = new FocasAlarmProjectionOptions
{
Enabled = alarmsEnabled,
PollInterval = TimeSpan.FromMilliseconds(30),
},
}, "drv-1", factory);
return (drv, factory);
}
[Fact]
public async Task Subscribe_without_Enable_throws_NotSupported()
{
var (drv, _) = NewDriver(alarmsEnabled: false);
await drv.InitializeAsync("{}", CancellationToken.None);
await Should.ThrowAsync<NotSupportedException>(() =>
drv.SubscribeAlarmsAsync([], CancellationToken.None));
}
[Fact]
public async Task Raise_then_clear_emits_both_events()
{
var (drv, factory) = NewDriver(alarmsEnabled: true);
factory.Customise = () => new FakeFocasClient();
await drv.InitializeAsync("{}", CancellationToken.None);
var events = new List<AlarmEventArgs>();
drv.OnAlarmEvent += (_, e) => { lock (events) events.Add(e); };
var sub = await drv.SubscribeAlarmsAsync([], CancellationToken.None);
// First tick creates the client via EnsureConnectedAsync — wait for it before we
// poke the alarm list so we don't race the poll loop.
await WaitFor(() => factory.Clients.Count > 0, TimeSpan.FromSeconds(3));
var client = factory.Clients[0];
client.Alarms.Add(new FocasActiveAlarm(500, FocasAlarmType.Overtravel, 1, "Axis 1 overtravel"));
await WaitFor(() => events.Any(e => e.Message.Contains("overtravel")), TimeSpan.FromSeconds(3));
// Clear — the clear event wraps the original message with "(cleared)".
client.Alarms.Clear();
await WaitFor(() => events.Any(e => e.Message.Contains("cleared")), TimeSpan.FromSeconds(3));
await drv.UnsubscribeAlarmsAsync(sub, CancellationToken.None);
await drv.ShutdownAsync(CancellationToken.None);
events.ShouldContain(e => e.AlarmType == "Overtravel" && e.Severity == AlarmSeverity.Critical);
events.ShouldContain(e => e.Message.Contains("cleared"));
events[0].SourceNodeId.ShouldBe(Host);
}
[Fact]
public async Task Tick_diffs_raises_and_clears_without_polling_loop()
{
// Drive Tick directly so the test isn't timing-dependent. The projection's
// Tick() is internal so we reach it through the driver using a handcrafted
// subscription — simpler than standing up the full loop.
var (drv, factory) = NewDriver(alarmsEnabled: true);
factory.Customise = () => new FakeFocasClient();
await drv.InitializeAsync("{}", CancellationToken.None);
var projection = new FocasAlarmProjection(drv, TimeSpan.FromMinutes(1));
var sub = new FocasAlarmProjection.Subscription(
new FocasAlarmSubscriptionHandle(1), deviceFilter: null,
new CancellationTokenSource());
var events = new List<AlarmEventArgs>();
drv.OnAlarmEvent += (_, e) => events.Add(e);
// Tick 1 — raise two alarms.
projection.Tick(sub, Host, [
new FocasActiveAlarm(100, FocasAlarmType.Parameter, 0, "Param 100"),
new FocasActiveAlarm(200, FocasAlarmType.Servo, 1, "Servo 200"),
]);
events.Count.ShouldBe(2);
events[0].Severity.ShouldBe(AlarmSeverity.Medium);
events[1].Severity.ShouldBe(AlarmSeverity.Critical);
// Tick 2 — same alarms stay active → no new events.
events.Clear();
projection.Tick(sub, Host, [
new FocasActiveAlarm(100, FocasAlarmType.Parameter, 0, "Param 100"),
new FocasActiveAlarm(200, FocasAlarmType.Servo, 1, "Servo 200"),
]);
events.ShouldBeEmpty();
// Tick 3 — one clears, one stays → one "cleared" event only.
projection.Tick(sub, Host, [
new FocasActiveAlarm(200, FocasAlarmType.Servo, 1, "Servo 200"),
]);
events.Count.ShouldBe(1);
events[0].Message.ShouldEndWith("(cleared)");
events[0].AlarmType.ShouldBe("Parameter");
}
[Fact]
public void Severity_mapping_matches_docs()
{
FocasAlarmProjection.MapSeverity(FocasAlarmType.Overtravel).ShouldBe(AlarmSeverity.Critical);
FocasAlarmProjection.MapSeverity(FocasAlarmType.Servo).ShouldBe(AlarmSeverity.Critical);
FocasAlarmProjection.MapSeverity(FocasAlarmType.PulseCode).ShouldBe(AlarmSeverity.Critical);
FocasAlarmProjection.MapSeverity(FocasAlarmType.Parameter).ShouldBe(AlarmSeverity.Medium);
FocasAlarmProjection.MapSeverity(FocasAlarmType.MacroAlarm).ShouldBe(AlarmSeverity.Medium);
FocasAlarmProjection.MapSeverity(FocasAlarmType.Overheat).ShouldBe(AlarmSeverity.High);
}
private static async Task WaitFor(Func<bool> pred, TimeSpan timeout)
{
var deadline = DateTime.UtcNow + timeout;
while (DateTime.UtcNow < deadline)
{
if (pred()) return;
await Task.Delay(30);
}
}
}