Rewrite src/ and tests/ project paths in docs, CLAUDE.md, README.md, and test-fixture READMEs to the new module-folder layout (Core/Server/Drivers/ Client/Tooling). References to retired v1 projects (Galaxy.Host/Proxy/Shared, the legacy monolithic test projects) are left untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
151 lines
7.2 KiB
Markdown
151 lines
7.2 KiB
Markdown
# 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`](https://github.com/Ladder99/focas-mock/tree/main/dotnet/Focas.Wire)
|
|
client. Integration tests run the managed driver end-to-end against the
|
|
vendored `focas-mock` Python server (at
|
|
[`tests/.../Docker/focas-mock/`](../../tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/focas-mock/VENDORED.md))
|
|
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](#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/Drivers/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`](../v2/focas-version-matrix.md).
|
|
- `FocasReadWriteTests` — read / write contract against the fake, FOCAS
|
|
native status → OPC UA `StatusCode` mapping
|
|
- `FocasScaffoldingTests` — `IDriver` 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/Drivers/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
|
|
|
|
```powershell
|
|
# 1) Start the mock on a chosen profile.
|
|
docker compose -f tests/Drivers/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/Drivers/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/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/focas-mock/`
|
|
— vendored `focas-mock` Python source + Dockerfile
|
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/docker-compose.yml`
|
|
— per-series compose profiles
|
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/FocasSimFixture.cs`
|
|
— collection fixture + mock admin API client
|
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Series/FixedTreePopulatesTests.cs`
|
|
— fixed-tree end-to-end tests
|
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Series/WireBackendTests.cs`
|
|
— pure-wire-backend end-to-end tests
|
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FakeFocasClient.cs` —
|
|
in-process unit fake
|
|
- `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/WireFocasClient.cs` — the
|
|
managed wire client backing production deployments
|
|
- `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasCapabilityMatrix.cs` —
|
|
per-series range validator
|
|
- `docs/v2/focas-version-matrix.md` — authoritative range reference
|