Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/AbCipEmulateAlmdTests.cs
Joseph Doherty a25593a9c6 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>
2026-05-17 01:55:28 -04:00

106 lines
5.0 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Emulate;
/// <summary>
/// Golden-box-tier ALMD alarm projection tests against Logix Emulate.
/// Promotes the feature-flagged ALMD projection (task #177) from unit-only coverage
/// (<c>AbCipAlarmProjectionTests</c> with faked InFaulted sequences) to end-to-end
/// wire-level coverage — Emulate runs the real ALMD instruction, with real
/// rising-edge semantics on <c>InFaulted</c> + <c>Ack</c>, so the driver's poll-based
/// projection gets validated against the actual behaviour shops running FT View see.
/// </summary>
/// <remarks>
/// <para><b>Required Emulate project state</b> (see <c>LogixProject/README.md</c>):</para>
/// <list type="bullet">
/// <item>Controller-scope ALMD tag <c>HighTempAlarm</c> — a standard ALMD instruction
/// with default member set (<c>In</c>, <c>InFaulted</c>, <c>Acked</c>,
/// <c>Severity</c>, <c>Cfg_ProgTime</c>, …).</item>
/// <item>A periodic task that drives <c>HighTempAlarm.In</c> false→true→false at a
/// cadence the operator can script via a one-shot routine (e.g. a
/// <c>SimulateAlarm</c> bit the test case pulses through
/// <c>IWritable.WriteAsync</c>).</item>
/// <item>Operator writes <c>1</c> to <c>SimulateAlarm</c> to trigger the rising
/// edge on <c>HighTempAlarm.In</c>; ladder uses that as the alarm input.</item>
/// </list>
/// <para>Runs only when <c>AB_SERVER_PROFILE=emulate</c>. ab_server has no ALMD
/// instruction + no alarm subsystem, so this tier-gated class couldn't produce a
/// meaningful result against the default simulator.</para>
/// </remarks>
[Collection("AbServerEmulate")]
[Trait("Category", "Integration")]
[Trait("Tier", "Emulate")]
public sealed class AbCipEmulateAlmdTests
{
[AbServerFact]
public async Task Real_ALMD_raise_fires_OnAlarmEvent_through_the_driver_projection()
{
AbServerProfileGate.SkipUnless(AbServerProfileGate.Emulate);
var endpoint = Environment.GetEnvironmentVariable("AB_SERVER_ENDPOINT")
?? throw new InvalidOperationException(
"AB_SERVER_ENDPOINT must be set to the Logix Emulate instance when AB_SERVER_PROFILE=emulate.");
var options = new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions($"ab://{endpoint}/1,0")],
EnableAlarmProjection = true,
AlarmPollInterval = TimeSpan.FromMilliseconds(200),
Tags = [
new AbCipTagDefinition(
Name: "HighTempAlarm",
DeviceHostAddress: $"ab://{endpoint}/1,0",
TagPath: "HighTempAlarm",
DataType: AbCipDataType.Structure,
Members: [
new AbCipStructureMember("InFaulted", AbCipDataType.DInt),
new AbCipStructureMember("Acked", AbCipDataType.DInt),
new AbCipStructureMember("Severity", AbCipDataType.DInt),
new AbCipStructureMember("In", AbCipDataType.DInt),
]),
// The "simulate the alarm input" bit the ladder watches.
new AbCipTagDefinition(
Name: "SimulateAlarm",
DeviceHostAddress: $"ab://{endpoint}/1,0",
TagPath: "SimulateAlarm",
DataType: AbCipDataType.Bool,
Writable: true),
],
};
await using var drv = new AbCipDriver(options, driverInstanceId: "emulate-almd");
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
var raised = new TaskCompletionSource<AlarmEventArgs>(TaskCreationOptions.RunContinuationsAsynchronously);
drv.OnAlarmEvent += (_, e) =>
{
if (e.Message.Contains("raised")) raised.TrySetResult(e);
};
var sub = await drv.SubscribeAlarmsAsync(
["HighTempAlarm"], TestContext.Current.CancellationToken);
// Pulse the input bit the ladder watches, then wait for the driver's poll loop
// to see InFaulted rise + fire the raise event.
_ = await drv.WriteAsync(
[new WriteRequest("SimulateAlarm", true)],
TestContext.Current.CancellationToken);
var got = await Task.WhenAny(raised.Task, Task.Delay(TimeSpan.FromSeconds(5)));
got.ShouldBe(raised.Task, "driver must surface the ALMD raise within 5 s of the ladder-driven edge");
var args = await raised.Task;
args.SourceNodeId.ShouldBe("HighTempAlarm");
args.AlarmType.ShouldBe("ALMD");
await drv.UnsubscribeAlarmsAsync(sub, TestContext.Current.CancellationToken);
// Reset the bit so the next test run starts from a known state.
_ = await drv.WriteAsync(
[new WriteRequest("SimulateAlarm", false)],
TestContext.Current.CancellationToken);
}
}