From 58a0cccc67048dd8325851ffa08f29d5ee54b618 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 20 Apr 2026 12:54:39 -0400 Subject: [PATCH] =?UTF-8?q?AB=20CIP=20Logix=20Emulate=20golden-box=20tier?= =?UTF-8?q?=20=E2=80=94=20scaffold=20the=20code=20+=20docs=20so=20the=20L5?= =?UTF-8?q?X=20+=20Emulate=20PC=20drop=20in=20without=20fixture-code=20cha?= =?UTF-8?q?nges.=20Closes=20the=20initial=20design=20question=20the=20user?= =?UTF-8?q?=20raised;=20the=20actual=20Emulate-side=20work=20(author=20pro?= =?UTF-8?q?ject,=20commit=20L5X,=20install=20Emulate=20on=20the=20dev=20bo?= =?UTF-8?q?x)=20is=20tracked=20as=20#223.=20Scaffolding=20ships=20everythi?= =?UTF-8?q?ng=20that=20doesn't=20need=20the=20live=20Emulate=20instance:?= =?UTF-8?q?=20tier-gated=20test=20classes=20that=20skip=20cleanly=20when?= =?UTF-8?q?=20`AB=5FSERVER=5FPROFILE`=20is=20unset,=20the=20profile=20gate?= =?UTF-8?q?=20helper,=20the=20LogixProject/README.md=20documenting=20the?= =?UTF-8?q?=20exact=20project=20state=20the=20tests=20expect,=20the=20fixt?= =?UTF-8?q?ure=20coverage=20doc's=20new=20=C2=A7Logix=20Emulate=20tier=20s?= =?UTF-8?q?ection=20with=20the=20when-to-trust=20table=20extended=20from?= =?UTF-8?q?=203=20columns=20to=204,=20and=20the=20dev-environment.md=20int?= =?UTF-8?q?egration-host=20row.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AbServerProfileGate — static helper that reads `AB_SERVER_PROFILE` env var (defaults to "abserver") + exposes `SkipUnless(params string[] requiredProfiles)` matching the MODBUS_SIM_PROFILE pattern the DL205StringQuirkTests uses one directory over. Emulate-only tests call `AbServerProfileGate.SkipUnless("emulate")` at the top of each fact body; ab_server-default runs see them skip with a clear message pointing at the Emulate setup steps. AbCipEmulateUdtReadTests — one test proving the #194 whole-UDT read optimization works against the real Logix Template Object, not just the golden byte buffers the unit suite uses. Builds an `AbCipDriverOptions` with a Structure tag `Motor1 : Motor_UDT` that has three declared members (Speed : DINT, Torque : REAL, Status : DINT), reads them via the `.Speed / .Torque / .Status` dotted-tag syntax, asserts the driver gets the grouped whole-UDT path + decodes each at the right offset. Required seed values documented inline + in LogixProject/README.md: Speed=1800, Torque=42.5f, Status=0x0001. AbCipEmulateAlmdTests — one test proving the #177 ALMD projection fires `OnAlarmEvent` when a real ALMD instruction's `In` edge rises, not just the fake `InFaulted` timer edges the unit suite drives. Needs a `SimulateAlarm : BOOL` tag routed through `MainRoutine` ladder (`XIC SimulateAlarm OTE HighTempAlarm.In`) so the test case can pulse the input via the existing `IWritable.WriteAsync` path instead of scripting Emulate via its own socket. Alarm-projection options carry `EnableAlarmProjection = true` + 200 ms poll interval; a `TaskCompletionSource` gates the raise-event assertion with a 5 s deadline. Cleanup writes SimulateAlarm=false so consecutive runs start from known state. LogixProject/README.md — the Studio 5000 project state the Emulate-tier tests depend on. Explains why L5X over ACD (text diff, reproducible import, no per-install state), the UDT + tag + routine structure, how to bring it up on the Emulate PC. Ships as a stub pending actual author + L5X export + commit; the README itself keeps the requirements visible so the L5X author has a checklist. docs/drivers/AbServer-Test-Fixture.md — new §Logix Emulate golden-box tier section with the coverage-promotion table (ab_server / Emulate / hardware per gap), the setup-env-var recipe, the costs to accept (license, Hyper-V conflict, manual lifecycle). "When to trust" table extended from 3 columns (ab_server / unit / rig) to 4 (ab_server / unit / Logix Emulate / rig); two new rows for EtherNet/IP embedded-switch + redundant-chassis failover that even Emulate can't help with. Follow-up candidates list gets Logix Emulate as option 1 ahead of the pre-existing "extend ab_server upstream" + "stand up a lab rig". See-also file list gains AbServerProfileGate.cs + Docker/ + Emulate/ + LogixProject/README.md entries. docs/v2/dev-environment.md — §C Integration host gains a Rockwell Studio 5000 Logix Emulate row: purpose (AB CIP golden-box tier closing UDT/ALMD/AOI/safety/ConnectionSize gaps), type (Windows-only, Hyper-V conflict matching TwinCAT XAR's constraint), port 44818, credentials note, owner split between integration-host admin for license+install and developer for per-session runtime start. Verified: Emulate tests skip cleanly when AB_SERVER_PROFILE is unset — both `[SKIP]` with the operator-facing message pointing at the env-var setup. Whole-solution build 0 errors. Tests will transition from skip → pass once the L5X + Emulate PC land per #223. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/drivers/AbServer-Test-Fixture.md | 101 +++++++++++++---- docs/v2/dev-environment.md | 1 + .../AbServerProfileGate.cs | 52 +++++++++ .../Emulate/AbCipEmulateAlmdTests.cs | 105 ++++++++++++++++++ .../Emulate/AbCipEmulateUdtReadTests.cs | 81 ++++++++++++++ .../LogixProject/README.md | 102 +++++++++++++++++ ...OpcUa.Driver.AbCip.IntegrationTests.csproj | 1 + 7 files changed, 424 insertions(+), 19 deletions(-) create mode 100644 tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfileGate.cs create mode 100644 tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/AbCipEmulateAlmdTests.cs create mode 100644 tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/AbCipEmulateUdtReadTests.cs create mode 100644 tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/LogixProject/README.md diff --git a/docs/drivers/AbServer-Test-Fixture.md b/docs/drivers/AbServer-Test-Fixture.md index d0530fa..a656d3c 100644 --- a/docs/drivers/AbServer-Test-Fixture.md +++ b/docs/drivers/AbServer-Test-Fixture.md @@ -116,38 +116,101 @@ No smoke test for: The driver implements all of these + they have unit coverage, but the only end-to-end path `ab_server` validates today is atomic `ReadAsync`. +## Logix Emulate golden-box tier + +Rockwell Studio 5000 Logix Emulate sits **above** ab_server in fidelity + +**below** real hardware. When an operator has Emulate running on a +reachable Windows box + sets two env vars, the suite promotes several +behaviours from unit-only to end-to-end wire-level coverage: + +```powershell +$env:AB_SERVER_PROFILE = 'emulate' +$env:AB_SERVER_ENDPOINT = ':44818' +dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests +``` + +With `AB_SERVER_PROFILE` unset or `abserver`, the Emulate-tier classes +skip cleanly + the ab_server Docker fixture runs as usual. + +| Gap this fixture doc calls out | ab_server | Logix Emulate | Real hardware | +|---|---|---|---| +| UDT / CIP Template Object (task #194) | no | **yes** | yes | +| ALMD alarm projection (task #177) | no | **yes** | yes | +| `@tags` Symbol Object walk with `Program:` scope | partial | **yes** | yes | +| Add-On Instructions | no | **yes** | yes | +| GuardLogix safety-partition write rejection | no | **yes** (Emulate 5580) | yes | +| CompactLogix narrow ConnectionSize enforcement | no | **yes** (5370 firmware) | yes | +| EtherNet/IP embedded-switch behaviour | no | no | yes | +| Redundant chassis failover (1756-RM) | no | no | yes | +| Motion control timing | no | no | yes | + +**Tests that promote to Emulate** (gated on `AB_SERVER_PROFILE=emulate` +via `AbServerProfileGate.SkipUnless`): + +- `AbCipEmulateUdtReadTests.WholeUdt_read_decodes_each_member_at_its_Template_Object_offset` + — #194 whole-UDT optimization, verified against real Template Object + bytes +- `AbCipEmulateAlmdTests.Real_ALMD_raise_fires_OnAlarmEvent_through_the_driver_projection` + — #177 ALMD projection, verified against the real ALMD instruction + +**Required Studio 5000 project state** is documented in +[`tests/…/AbCip.IntegrationTests/LogixProject/README.md`](../../tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/LogixProject/README.md); +the `.L5X` export lands there once the Emulate PC is on-site + the +project is authored. + +**Costs to accept**: + +- **Rockwell TechConnect or per-seat license** — not redistributable; + not CI-runnable. Each operator licenses their own Emulate install. +- **Windows-only + Hyper-V conflict** — Emulate can't coexist with + Docker Desktop's WSL 2 backend on the same OS, same way TwinCAT XAR + can't (see `docs/v2/dev-environment.md` §Integration host). +- **Manual lifecycle** — no `docker compose up` equivalent; operator + opens Emulate, loads the L5X, clicks Run. The L5X in the repo keeps + project state reproducible, runtime-start is human. + ## When to trust ab_server, when to reach for a rig -| Question | ab_server | Unit tests | Lab rig | -| --- | --- | --- | --- | -| "Does the driver talk CIP at all?" | yes | - | - | -| "Is my atomic read path wired correctly?" | yes | yes | yes | -| "Does whole-UDT grouping work?" | no | yes | yes (required) | -| "Do ALMD alarms raise + clear?" | no | yes | yes (required) | -| "Is Micro800 unconnected-only enforced wire-side?" | no (emulated as CLX) | partial | yes (required) | -| "Does GuardLogix reject non-safety writes on safety tags?" | no | no | yes (required) | -| "Does CompactLogix refuse oversized ConnectionSize?" | no | partial | yes (required) | -| "Does BOOL-in-DINT RMW race against concurrent writers?" | no | yes | yes (stress) | +| Question | ab_server | Unit tests | Logix Emulate | Lab rig | +| --- | --- | --- | --- | --- | +| "Does the driver talk CIP at all?" | yes | - | yes | - | +| "Is my atomic read path wired correctly?" | yes | yes | yes | yes | +| "Does whole-UDT grouping work?" | no | yes | **yes** | yes | +| "Do ALMD alarms raise + clear?" | no | yes | **yes** | yes | +| "Is Micro800 unconnected-only enforced wire-side?" | no (emulated as CLX) | partial | yes | yes (required) | +| "Does GuardLogix reject non-safety writes on safety tags?" | no | no | yes (Emulate 5580) | yes | +| "Does CompactLogix refuse oversized ConnectionSize?" | no | partial | yes (5370 firmware) | yes | +| "Does BOOL-in-DINT RMW race against concurrent writers?" | no | yes | partial | yes (stress) | +| "Does EtherNet/IP embedded-switch behave correctly?" | no | no | no | yes (required) | +| "Does redundant-chassis failover work?" | no | no | no | yes (required) | ## Follow-up candidates If integration-level UDT / alarm / quirk proof becomes a shipping gate, the options are roughly: -1. **Extend `ab_server`** upstream — the project accepts PRs + already carries - a CIP framing layer that UDT emulation could plug into. -2. **Swap in a richer simulator** — e.g. - [OpenPLC](https://www.openplcproject.com/) or pycomm3's test harness, if - either exposes UDTs over EtherNet/IP in a way libplctag can consume. -3. **Stand up a lab rig** — physical `1756-L7x` / `5069-L3x` / `2080-LC30` / - `1756-L8xS` controllers running Rockwell Studio 5000 projects; wire into - CI via a self-hosted runner. The only path that covers safety + narrow - ConnectionSize + real ALMD execution. +1. **Logix Emulate golden-box tier** (scaffolded; see the section above) — + highest-fidelity path short of real hardware. Closes UDT / ALMD / AOI / + optimized-DB gaps in one license + one Windows PC. +2. **Extend `ab_server`** upstream — the project accepts PRs + already + carries a CIP framing layer that UDT emulation could plug into. +3. **Stand up a lab rig** — physical `1756-L7x` / `5069-L3x` / `2080-LC30` + / `1756-L8xS` controllers. The only path that covers safety partitions + across nodes, redundant chassis, embedded-switch behaviour, and motion + timing. See also: - `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerFixture.cs` - `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfile.cs` +- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfileGate.cs` + — `AB_SERVER_PROFILE` tier gate - `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbCipReadSmokeTests.cs` +- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/` — ab_server + image + compose +- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/` — Logix + Emulate tier tests +- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/LogixProject/README.md` + — L5X project state the Emulate tier expects - `docs/v2/test-data-sources.md` §2 — the broader test-data-source picking rationale this fixture slots into diff --git a/docs/v2/dev-environment.md b/docs/v2/dev-environment.md index 327565c..8b6d67b 100644 --- a/docs/v2/dev-environment.md +++ b/docs/v2/dev-environment.md @@ -149,6 +149,7 @@ Dev credentials in this inventory are convenience defaults, not secrets. Change | **S7 fixture — `otopcua-python-snap7:1.0`** | S7 driver integration tests | Docker image (local build, `python-snap7>=2.0`; see `tests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Docker/`); 1 compose profile: `s7_1500` | 1102 (non-privileged; driver honours `S7DriverOptions.Port`) | n/a | Developer (per machine) | | **OPC UA Client fixture — `mcr.microsoft.com/iotedge/opc-plc:2.14.10`** | OpcUaClient driver integration tests | Docker image (Microsoft-maintained, pinned; see `tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/`) | 50000 (OPC UA) | Anonymous (`--daa` off); auto-accept certs (`--aa`) | Developer (per machine) | | **TwinCAT XAR runtime VM** | TwinCAT ADS testing (per `test-data-sources.md` §5; Beckhoff XAR cannot coexist with Hyper-V on the same OS) | Hyper-V VM with Windows + TwinCAT XAR installed under 7-day renewable trial | 48898 (ADS over TCP) | TwinCAT default route credentials configured per Beckhoff docs | Integration host admin | +| **Rockwell Studio 5000 Logix Emulate** | AB CIP golden-box tier — closes UDT / ALMD / AOI / GuardLogix-safety / CompactLogix-ConnectionSize gaps the ab_server simulator can't cover. Loads the L5X project documented at `tests/.../AbCip.IntegrationTests/LogixProject/README.md`. Tests gated on `AB_SERVER_PROFILE=emulate` + `AB_SERVER_ENDPOINT=:44818`; see `docs/drivers/AbServer-Test-Fixture.md` §Logix Emulate golden-box tier | Windows-only install; **Hyper-V conflict** — can't coexist with Docker Desktop's WSL 2 backend on the same OS, same story as TwinCAT XAR. Runs on a dedicated Windows PC reachable on the LAN | 44818 (CIP / EtherNet/IP) | None required at the CIP layer; Studio 5000 project credentials per Rockwell install | Integration host admin (license + install); Developer (per session — open Emulate, load L5X, click Run) | | **FOCAS TCP stub** (`Driver.Focas.TestStub`) | FOCAS functional testing (per `test-data-sources.md` §6) | Local .NET 10 console app from this repo | 8193 (FOCAS) | n/a | Developer / integration host (run on demand) | | **FOCAS FaultShim** (`Driver.Focas.FaultShim`) | FOCAS native-fault injection (per `test-data-sources.md` §6) | Test-only native DLL named `Fwlib64.dll`, loaded via DLL search path in the test fixture | n/a (in-process) | n/a | Developer / integration host (test-only) | diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfileGate.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfileGate.cs new file mode 100644 index 0000000..e4ac2dc --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfileGate.cs @@ -0,0 +1,52 @@ +using Xunit; + +namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests; + +/// +/// Runtime gate that lets an integration-test class declare which target-server tier +/// it requires. Reads AB_SERVER_PROFILE from the environment; tests call +/// with the profile names they support + skip otherwise. +/// +/// +/// Two tiers today: +/// +/// abserver (default) — the Dockerized libplctag ab_server +/// simulator. Covers atomic reads / writes / basic discovery across the four +/// families (ControlLogix / CompactLogix / Micro800 / GuardLogix). +/// emulate — Rockwell Studio 5000 Logix Emulate on an operator's +/// Windows box, exposed via AB_SERVER_ENDPOINT. Adds real UDT / ALMD / +/// AOI / Program-scoped-tag coverage that ab_server can't emulate. Tier-gated +/// because Emulate is per-seat licensed + Windows-only + manually launched; +/// a stock `dotnet test` run against ab_server must skip Emulate-only classes +/// cleanly. +/// +/// Tests assert their target tier at the top of each [Fact] / +/// [Theory] body, mirroring the MODBUS_SIM_PROFILE gate pattern in +/// tests/.../Modbus.IntegrationTests/DL205/DL205StringQuirkTests.cs. +/// +public static class AbServerProfileGate +{ + public const string Default = "abserver"; + public const string Emulate = "emulate"; + + /// Active profile from AB_SERVER_PROFILE; defaults to . + public static string CurrentProfile => + Environment.GetEnvironmentVariable("AB_SERVER_PROFILE") is { Length: > 0 } raw + ? raw.Trim().ToLowerInvariant() + : Default; + + /// + /// Skip the calling test via Assert.Skip when + /// isn't in . Case-insensitive match. + /// + public static void SkipUnless(params string[] requiredProfiles) + { + foreach (var p in requiredProfiles) + if (string.Equals(p, CurrentProfile, StringComparison.OrdinalIgnoreCase)) + return; + Assert.Skip( + $"Test requires AB_SERVER_PROFILE in {{{string.Join(", ", requiredProfiles)}}}; " + + $"current value is '{CurrentProfile}'. " + + $"Set AB_SERVER_PROFILE=emulate + point AB_SERVER_ENDPOINT at a Logix Emulate instance to run the golden-box-tier tests."); + } +} diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/AbCipEmulateAlmdTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/AbCipEmulateAlmdTests.cs new file mode 100644 index 0000000..0624b88 --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/AbCipEmulateAlmdTests.cs @@ -0,0 +1,105 @@ +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; + +/// +/// Golden-box-tier ALMD alarm projection tests against Logix Emulate. +/// Promotes the feature-flagged ALMD projection (task #177) from unit-only coverage +/// (AbCipAlarmProjectionTests with faked InFaulted sequences) to end-to-end +/// wire-level coverage — Emulate runs the real ALMD instruction, with real +/// rising-edge semantics on InFaulted + Ack, so the driver's poll-based +/// projection gets validated against the actual behaviour shops running FT View see. +/// +/// +/// Required Emulate project state (see LogixProject/README.md): +/// +/// Controller-scope ALMD tag HighTempAlarm — a standard ALMD instruction +/// with default member set (In, InFaulted, Acked, +/// Severity, Cfg_ProgTime, …). +/// A periodic task that drives HighTempAlarm.In false→true→false at a +/// cadence the operator can script via a one-shot routine (e.g. a +/// SimulateAlarm bit the test case pulses through +/// IWritable.WriteAsync). +/// Operator writes 1 to SimulateAlarm to trigger the rising +/// edge on HighTempAlarm.In; ladder uses that as the alarm input. +/// +/// Runs only when AB_SERVER_PROFILE=emulate. ab_server has no ALMD +/// instruction + no alarm subsystem, so this tier-gated class couldn't produce a +/// meaningful result against the default simulator. +/// +[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(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); + } +} diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/AbCipEmulateUdtReadTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/AbCipEmulateUdtReadTests.cs new file mode 100644 index 0000000..3386629 --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/AbCipEmulateUdtReadTests.cs @@ -0,0 +1,81 @@ +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; + +/// +/// Golden-box-tier UDT read tests against Rockwell Studio 5000 Logix Emulate. +/// Promotes the whole-UDT-read optimization (task #194) from unit-only coverage +/// (golden Template Object byte buffers + ) +/// to end-to-end wire-level coverage — Emulate's firmware speaks the same CIP +/// Template Object responses real hardware does, so the member-offset math + the +/// AbCipUdtReadPlanner grouping get validated against production semantics. +/// +/// +/// Required Emulate project state (see LogixProject/README.md +/// for the L5X export that seeds this; ship the project once Emulate is on the +/// integration host): +/// +/// UDT Motor_UDT with members Speed : DINT, Torque : REAL, +/// Status : DINT — the member set +/// uses as its declared-layout golden reference. +/// Controller-scope tag Motor1 : Motor_UDT with seed values +/// Speed=1800, Torque=42.5f, Status=0x0001. +/// +/// Runs only when AB_SERVER_PROFILE=emulate. With ab_server +/// (the default), skips cleanly — ab_server lacks UDT / Template Object emulation +/// so this wire-level test couldn't pass against it regardless. +/// +[Collection("AbServerEmulate")] +[Trait("Category", "Integration")] +[Trait("Tier", "Emulate")] +public sealed class AbCipEmulateUdtReadTests +{ + [AbServerFact] + public async Task WholeUdt_read_decodes_each_member_at_its_Template_Object_offset() + { + 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 " + + "(e.g. '10.0.0.42:44818') when AB_SERVER_PROFILE=emulate."); + + var options = new AbCipDriverOptions + { + Devices = [new AbCipDeviceOptions($"ab://{endpoint}/1,0")], + Tags = [ + new AbCipTagDefinition( + Name: "Motor1", + DeviceHostAddress: $"ab://{endpoint}/1,0", + TagPath: "Motor1", + DataType: AbCipDataType.Structure, + Members: [ + new AbCipStructureMember("Speed", AbCipDataType.DInt), + new AbCipStructureMember("Torque", AbCipDataType.Real), + new AbCipStructureMember("Status", AbCipDataType.DInt), + ]), + ], + Timeout = TimeSpan.FromSeconds(5), + }; + + await using var drv = new AbCipDriver(options, driverInstanceId: "emulate-udt-smoke"); + await drv.InitializeAsync("{}", TestContext.Current.CancellationToken); + + // Whole-UDT read optimization from task #194: one libplctag read on the + // parent tag, three decodes from the buffer at member offsets. Asserts + // Emulate's Template Object response matches what AbCipUdtMemberLayout + // computes from the declared member set. + var snapshots = await drv.ReadAsync( + ["Motor1.Speed", "Motor1.Torque", "Motor1.Status"], + TestContext.Current.CancellationToken); + + snapshots.Count.ShouldBe(3); + foreach (var s in snapshots) s.StatusCode.ShouldBe(AbCipStatusMapper.Good); + Convert.ToInt32(snapshots[0].Value).ShouldBe(1800); + Convert.ToSingle(snapshots[1].Value).ShouldBe(42.5f, tolerance: 0.001f); + Convert.ToInt32(snapshots[2].Value).ShouldBe(0x0001); + } +} diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/LogixProject/README.md b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/LogixProject/README.md new file mode 100644 index 0000000..a7dc6dd --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/LogixProject/README.md @@ -0,0 +1,102 @@ +# Logix Emulate project stub + +This folder holds the Studio 5000 project that Logix Emulate loads when running +the Emulate-tier integration tests +(`tests/.../AbCip.IntegrationTests/Emulate/*.cs`, gated on +`AB_SERVER_PROFILE=emulate`). + +**Status today**: stub. The actual `.L5X` export isn't committed yet; once +the Emulate PC is running + a project with the required state exists, +export to L5X + drop it here as `OtOpcUaAbCipFixture.L5X`. + +## Why L5X, not .ACD + +Studio 5000 ships two save formats: `.ACD` (binary, the runtime project) +and `.L5X` (XML export). Ship the L5X because: + +- Text format — reviewable in PR diffs, diffable in git +- Reproducible import across Studio 5000 versions +- Doesn't carry per-installation state (license watermarks, revision history) + +Reconstruction workflow: Studio 5000 → open project → File → Save As +→ `.L5X`. On a fresh Emulate install: File → Open → select the L5X → it +rebuilds the ACD from the XML. + +## Required project state + +The Emulate-tier tests rely on this exact tag / UDT set. Missing any of +these makes the dependent test fail loudly (TagNotFound, wrong value, +wrong type), not skip silently — Emulate is a tier above the Docker +simulator; operators who opted into it get opt-in-level coverage +expectations. + +### UDT definitions + +| UDT name | Members | Notes | +|---|---|---| +| `Motor_UDT` | `Speed : DINT`, `Torque : REAL`, `Status : DINT` | Matches `AbCipUdtMemberLayoutTests` declared-layout golden. Member order fixed — Logix Template Object offsets depend on it | + +### Controller tags + +| Tag | Type | Seed value | Purpose | +|---|---|---|---| +| `Motor1` | `Motor_UDT` | `{Speed=1800, Torque=42.5, Status=0x0001}` | `AbCipEmulateUdtReadTests.WholeUdt_read_decodes_each_member_at_its_Template_Object_offset` | +| `HighTempAlarm` | `ALMD` | default ALMD config, `In` tied to `SimulateAlarm` bit | `AbCipEmulateAlmdTests.Real_ALMD_raise_fires_OnAlarmEvent_through_the_driver_projection` | +| `SimulateAlarm` | `BOOL` | `0` | Operator-writable bit the ladder routes into `HighTempAlarm.In` — gives the test a clean way to drive the alarm edge without scripting Emulate directly | + +### Program structure + +- One periodic task `MainTask` @ 100 ms +- One program `MainProgram` +- One routine `MainRoutine` (Ladder) with a single rung: + `XIC SimulateAlarm OTE HighTempAlarm.In` + +That's enough ladder for `SimulateAlarm := 1` to raise the alarm + for +`SimulateAlarm := 0` to clear it. + +## Tier-level behaviours this project enables + +Coverage the existing Dockerized `ab_server` fixture can't produce — +each verified by an `Emulate/*Tests.cs` class gated on +`AB_SERVER_PROFILE=emulate`: + +- **CIP Template Object round-trip** — real Logix template bytes, + reads produce the same offset layout the CIP Symbol Object decoder + + `AbCipUdtMemberLayout` expect. +- **ALMD rising-edge semantics** — real Logix ALMD instruction fires + `InFaulted` / `Acked` transitions at cycle boundaries, not at our + unit-test fake's timer boundaries. +- **Optimized vs unoptimized DB behaviour** — Logix 5380/5580 series + runs the Studio 5000 project with optimized-DB-equivalent member + access; the driver's read path exercises that wire surface. + +Not in scope even with Emulate — needs real hardware: + +- EtherNet/IP embedded-switch behaviour (Stratix 5700, 1756-EN4TR) +- CIP Safety across partitions (Emulate 5580 emulates safety within + the chassis but not across nodes) +- Redundant chassis failover (1756-RM) +- Motion control timing +- High-speed discrete-input scheduling + +## How to run the Emulate-tier tests + +On the dev box: + +```powershell +$env:AB_SERVER_PROFILE = 'emulate' +$env:AB_SERVER_ENDPOINT = '10.0.0.42:44818' # replace with the Emulate PC IP +dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests +``` + +With `AB_SERVER_PROFILE` unset or `abserver`, the `Emulate/*Tests.cs` +classes skip cleanly; ab_server-backed tests run as usual. + +## See also + +- [`docs/drivers/AbServer-Test-Fixture.md`](../../../docs/drivers/AbServer-Test-Fixture.md) + §Logix Emulate golden-box tier — coverage map +- [`docs/v2/dev-environment.md`](../../../docs/v2/dev-environment.md) + §Integration host — license + networking notes +- Studio 5000 Logix Designer + Logix Emulate product pages on the + Rockwell TechConnect portal (licensed; internal link only). diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.csproj b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.csproj index 3f97c87..6a84f02 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.csproj +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.csproj @@ -25,6 +25,7 @@ + -- 2.49.1