Files
lmxopcua/docs/drivers/FOCAS-Test-Fixture.md
Joseph Doherty 4b0664bd55 FOCAS — retire Tier-C split, inline managed wire client, make read-only
Migration closes the FOCAS Tier-C architecture. OtOpcUa previously had
`Driver.FOCAS.Host` (NSSM-wrapped Windows service loading Fwlib64.dll via
P/Invoke) + `Driver.FOCAS.Shared` (MessagePack IPC contracts) + a C shim
DLL stand-in for unit tests. All of it is deleted; the driver is now a
single in-process managed assembly talking the FOCAS/2 Ethernet binary
protocol directly on TCP:8193.

Architecture

- Pure-managed `FocasWireClient` inlined at `src/.../Driver.FOCAS/Wire/`
  (owner-imported — see Wire/FocasWireClient.cs for the full surface).
  Opens two TCP sockets, runs the initiate handshake, serialises requests
  on socket 2 through a semaphore, closes cleanly with PDU + socket
  teardown. Both sync `IDisposable` and async `IAsyncDisposable`.
- `WireFocasClient` (same folder) adapts the wire client to OtOpcUa's
  `IFocasClient` surface — fixed-tree reads, PARAM/MACRO/PMC addresses,
  alarms. Writes return `BadNotWritable` by design — OtOpcUa is read-only
  against FOCAS.
- `FocasDriverFactoryExtensions` now accepts `"Backend": "wire"` (default)
  and `"Backend": "unimplemented"`. Legacy `ipc` and `fwlib` backends are
  rejected at startup with a diagnostic pointing at the migration doc.

Deletions

- `src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host/` — whole project + Ipc/,
  Backend/, Stability/, Program.cs.
- `src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared/` — Contracts/, FrameReader,
  FrameWriter, whole project.
- `tests/...Driver.FOCAS.Host.Tests/` + `.Shared.Tests/` — whole projects.
- `src/.../Driver.FOCAS/FwlibNative.cs` + `FwlibFocasClient.cs` — 21
  P/Invokes + 7 `Pack=1` marshalling structs + the Fwlib-backed
  `IFocasClient` implementation.
- `src/.../Driver.FOCAS/Ipc/` + `Supervisor/` — IPC client wrapper +
  Host-process supervisor (backoff, circuit breaker, heartbeat, post-
  mortem reader, process launcher).
- `scripts/install/Install-FocasHost.ps1` — NSSM service installer.
- `tests/.../Driver.FOCAS.Tests/{IpcFocasClientTests, IpcLoopback,
  FwlibNativeHelperTests, PostMortemReaderCompatibilityTests,
  SupervisorTests, FocasDriverFactoryExtensionsTests}.cs` — tests that
  exercised the retired surfaces.
- `tests/.../Driver.FOCAS.IntegrationTests/Shim/` — the zig-built C shim
  DLL that masqueraded as Fwlib64.dll.

Solution changes

- `ZB.MOM.WW.OtOpcUa.slnx` drops the 4 retired project refs.
- `src/.../Driver.FOCAS.csproj` drops the Shared ProjectReference, adds
  `Microsoft.Extensions.Logging.Abstractions` for the optional `ILogger`
  hook in `FocasWireClient`.
- `src/.../Driver.FOCAS.Cli.csproj` drops the six `<Content Include>`
  entries that copied `vendor/fanuc/*.dll` into the CLI bin. CLI now uses
  `WireFocasClient` directly.
- `FocasDriver` default factory flips to `Wire.WireFocasClientFactory`.

Integration tests

- New `tests/.../Driver.FOCAS.IntegrationTests/` project covering fixed-
  tree reads (identity, axes, dynamic, program, operation mode, timers,
  spindle load + max RPM, servo meters), user-authored PARAM / MACRO /
  PMC reads, `DiscoverAsync` emission, `SubscribeAsync` + `OnDataChange`,
  `IAlarmSource` raise/clear transitions, and `ProbeAsync` /
  `OnHostStatusChanged`. 9 e2e tests against the focas-mock fixture
  (Docker container with the vendored Python mock's native FOCAS/2
  Ethernet responder).
- `scripts/integration/run-focas.ps1` orchestrates compose up → tests →
  compose down. Dropped the shim-build stage + DLL-copy step + the split
  testhost workaround (the latter only existed because of native-DLL
  lifecycle bugs the shim tripped).
- Docker compose collapses from 11 per-series services to one `focas-sim`
  service. Tests seed per-series state via `mock_load_profile` at test
  start.
- Vendored focas-mock snapshot refreshed to pick up upstream's native
  FOCAS/2 Ethernet responder (was 660 lines, now 1018) — the
  pre-refresh snapshot only spoke the JSON admin protocol.

Tests

- 145/145 unit tests in `Driver.FOCAS.Tests` pass (was 208 pre-deletion;
  63 removed tests exercised the retired IPC/shim/supervisor/Fwlib
  surfaces).
- 9/9 integration tests pass against the refreshed mock.
- `FocasScaffoldingTests.Unimplemented_factory_throws_on_Create…` updated
  to assert the new diagnostic message pointing at
  `docs/drivers/FOCAS.md` rather than the now-gone `Fwlib64.dll`.

Docs

- `docs/drivers/FOCAS.md` rewritten for the managed wire topology —
  deployment collapses to one `"Backend": "wire"` config block, no
  separate service, no DLL deployment, no pipe ACL.
- `docs/drivers/FOCAS-Test-Fixture.md` updated — single TCP probe skip
  gate instead of TCP + shim probe; fewer moving parts.
- `docs/drivers/README.md` row for FOCAS reflects the Tier-A managed
  topology (previously listed Tier-C + `Fwlib64.dll` P/Invoke).
- `docs/Driver.FOCAS.Cli.md` drops the Tier-C architecture-note section.
- `docs/v2/implementation/focas-isolation-plan.md` marked historical —
  the plan it documents was executed then superseded by the wire client.
- `docs/v2/v2-release-readiness.md` re-audited 2026-04-24. Phase 5
  driver complement closed. FOCAS change-log entry added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:10:59 -04:00

7.1 KiB

FOCAS test fixture

Coverage map + gap inventory for the FANUC FOCAS2 CNC driver.

Status: as of 2026-04-24, OtOpcUa speaks FOCAS2 directly over TCP via the pure-managed Focas.Wire client. Integration tests run the managed driver end-to-end against the vendored focas-mock Python server (at tests/.../Docker/focas-mock/) whose native FOCAS Ethernet responder is verified PDU-by-PDU against the real fwlibe64.dll.

No shim DLL, no P/Invoke, no licensed binary — any dev box or CI runner with Docker can run the full fixture end-to-end.

Hardware validation against a real CNC is still useful to catch series-specific firmware quirks (see § Hardware-only gaps) but the mock's wire responder covers every FOCAS call OtOpcUa issues.

What the fixture covers

Unit layer (no container required)

tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/ uses FakeFocasClient injected via IFocasClientFactory:

  • FocasCapabilityTests — data-type mapping (PMC bit / byte / word / long / float / double, macro variable types, parameter types)
  • FocasCapabilityMatrixTests — per-CNC-series range validation across 16i / 0i-D / 0i-F / 30i / Power Motion, 46 theory cases locking every documented range boundary. See docs/v2/focas-version-matrix.md.
  • FocasReadWriteTests — read / write contract against the fake, FOCAS native status → OPC UA StatusCode mapping
  • FocasScaffoldingTestsIDriver lifecycle + multi-device routing
  • FocasPmcBitRmwTests — PMC bit read-modify-write synchronisation
  • FocasAlarmProjectionTests — raise / clear diffing, severity mapping
  • FocasHandleRecycleTests — proactive session recycle cadence

Capability surfaces whose contract is verified: IDriver, IReadable, ITagDiscovery, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IAlarmSource. IWritable intentionally returns BadNotWritable — OtOpcUa is read-only against FOCAS.

Pre-flight validation runs in FocasDriver.InitializeAsync — configs referencing out-of-range addresses fail at load time with a diagnostic message naming the CNC series + documented limit.

Integration layer (mock only, no CNC, no shim)

tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/ drives the managed FocasDriver end-to-end. A single gate:

Docker compose up — tests skip when the TCP probe to localhost:8193 fails with a pointer to the compose command.

When the mock is up, WireFocasClient dials it over TCP exactly like a real CNC, and the mock's native FOCAS Ethernet responder replies with binary PDUs against the documented command IDs. Covered assertions:

  • Session open / close (cnc_allclibhndl3 + cnc_freelibhndl)
  • Parameter read-back after mock_patch seed → cnc_rdparam
  • Macro read-back after seed → cnc_rdmacro (scaled-decimal translation verified)
  • PMC range read after seed → pmc_rdpmcrng
  • IAlarmSource raise + clear transitions after mock_patch alarm-list changes → cnc_rdalmmsg2
  • Fixed-tree bootstrap: identity / axes / spindle / program / timers / servo meters populate via cnc_sysinfo, cnc_rdaxisname, cnc_rdspdlname, cnc_rddynamic2, cnc_exeprgname2, cnc_rdblkcount, cnc_rdopmode, cnc_rdsvmeter, cnc_rdspload, cnc_rdspmaxrpm, cnc_rdtimer
  • Per-series profile selection via mock_load_profile — tests can pin one profile and assert series-gated capability suppression

E2E script (CLI)

scripts/e2e/test-focas.ps1 drives the Client.CLI against a running OtOpcUa server. Accepts:

  • -CncHost / -CncPort for real hardware
  • -ProfileName <compose-profile> for the Docker mock
  • -Series <csv> for per-series matrix mode
  • -HandleLeakCycles <N> for handle-leak stress

Hardware-only gaps

The mock has parity with the real fwlibe64.dll for the calls OtOpcUa issues, but a real CNC can still surface things a reference implementation can't:

  1. Series-specific firmware quirks — alarm retention across power cycles, parameter range enforcement by the CNC (not the driver), MTB custom screens, series-specific option bits. Each series has documented behaviours that only a bench CNC exercises.
  2. Wire-level stress — burst reads, concurrent device writes, network-partition recovery under load. The mock handles these correctly but production behaviour is the source of truth.
  3. Transient operational states — alarm floods, emergency-stop transitions, power-on resync. These are easy to stub but hard to cover comprehensively in the mock.

Track the close-out under task #54 (live-CNC smoke). When the rig lands, the hardware path runs alongside the mock path; the mock stays as the CI quality gate.

When to trust each layer

Question Unit Integration (mock) Real CNC
"Does PMC address R100.3 route to the right bit?"
"Does the Fanuc status → OPC UA StatusCode map cover every documented code?" (contract)
"Does FocasDriver.ReadAsync correctly decode a seeded parameter?" no
"Does IAlarmSource fire raise + clear events?" (Fake) (wire)
"Does a real read against a 30i Series return correct bytes?" no (via profile) (required)
"Do series-specific firmware quirks behave as documented?" no no (required)
"Does the driver survive real network partitions?" no partial (socket kill) (required)

Running the integration fixture

# 1) Start the mock on a chosen profile.
docker compose -f tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/docker-compose.yml up -d

# 2) Run the tests. No shim build, no DLL copy — the driver dials the mock directly.
dotnet test tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/

Or use scripts/integration/run-focas.ps1 which wraps compose up / test / compose down and accepts -Profile <name> to pin a per-series run.

Key fixture / config files

  • tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/focas-mock/ — vendored focas-mock Python source + Dockerfile
  • tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/docker-compose.yml — per-series compose profiles
  • tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/FocasSimFixture.cs — collection fixture + mock admin API client
  • tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Series/FixedTreePopulatesTests.cs — fixed-tree end-to-end tests
  • tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Series/WireBackendTests.cs — pure-wire-backend end-to-end tests
  • tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FakeFocasClient.cs — in-process unit fake
  • src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/WireFocasClient.cs — the managed wire client backing production deployments
  • src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasCapabilityMatrix.cs — per-series range validator
  • docs/v2/focas-version-matrix.md — authoritative range reference