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,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user