Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/Docker/README.md
T
Joseph Doherty cf9ce91e51 test(mtconnect): live-Agent docker fixture + integration suite (Tasks 19+20)
The first and only thing in the MTConnect workstream that exercises the driver
against a real Agent; everything else runs on canned XML.

Fixture (Docker/): two services, both stock images with this folder bind-mounted.
- `agent`   mtconnect/agent:2.7.0.12 on :5000. NOTE the repo name: the source
            project is mtconnect/cppagent but the PUBLISHED image is
            mtconnect/agent; mtconnect/cppagent does not exist on Docker Hub.
- `adapter` a stdlib SHDR feeder. Required, not optional: the agent image ships
            only the binary + schemas/styles, so a lone Agent answers /probe and
            then reports every observation UNAVAILABLE forever, which can prove
            nothing about reads or streaming.

Devices.xml seeds one DataItem per inference branch (SAMPLE+units, TIME_SERIES,
PART_COUNT/LINE_NUMBER, controlled vocab, DATA_SET, CONDITION), gives every named
item `name != id` (the inverse of the hand-authored unit fixtures), and leaves one
EVENT permanently unfed so UNAVAILABLE -> BadNoCommunication has a live subject.

Suite (12 tests) asserts STRUCTURALLY against the Agent's own /probe response, never
by literal id, so it survives an edit to Devices.xml and can be pointed at a real
machine tool via MTCONNECT_AGENT_ENDPOINT. It skips cleanly (12/12) when no Agent
answers, and each test carries a hard [Fact(Timeout)] so a half-up fixture cannot
wedge a build.

Three findings from bringing the fixture up, each now pinned in a comment:
- agent.cfg must be pure ASCII; one non-ASCII byte in a COMMENT makes the config
  parser reject the whole file with a bare "Failed / Stopped at line: N" and exit.
- `sampleCount` is an attribute of a TIME_SERIES observation, not of a DataItem
  declaration. A real Agent meeting one on a declaration DROPS THE ENTIRE DATA ITEM
  from the device model, so the inference only ever sees null and a live TIME_SERIES
  tag is always variable-length. Asserted, not ignored.
- The Agent's own <Agent> self-model publishes update-rate SAMPLEs that tick whether
  or not any adapter is attached. Excluding them is load-bearing: with them included
  the "live stream delivered a changed value" test passed with the fixture's data
  source deliberately stopped.

MTConnectError-under-HTTP-200 is NOT covered live and cannot be: cppagent 2.7 answers
an unknown device 404 and an out-of-range sequence 400, each with a well-formed error
body. That shape stays canned-only, and the suite says so.
2026-07-27 13:58:54 -04:00

106 lines
5.6 KiB
Markdown

# MTConnect integration-test fixture — the official C++ Agent + an SHDR data source
The MTConnect C++ Agent (`mtconnect/agent`, pinned) serving a canned device model, fed live
data by a standard-library SHDR adapter. No image build step: both services run stock images
with this folder's files bind-mounted.
> **The published image is `mtconnect/agent`, not `mtconnect/cppagent`.** `cppagent` is the
> name of the source project on GitHub; there is no Docker Hub repository under that name.
| File | Purpose |
|---|---|
| [`docker-compose.yml`](docker-compose.yml) | Two services: `agent` (published on :5000) and `adapter` (internal, :7878) |
| [`agent.cfg`](agent.cfg) | Agent configuration, bind-mounted at `/mtconnect/config/agent.cfg` (the image's CMD path) |
| [`Devices.xml`](Devices.xml) | The canned device model the Agent serves from `/probe` |
| [`adapter.py`](adapter.py) | SHDR feeder — the data source. Pure stdlib, runs on a stock `python:*-alpine` |
## Why there is an adapter service
The `mtconnect/agent` image ships **only** the agent binary plus its schemas and styles — there
is no bundled simulator. An Agent with no adapter answers `/probe` correctly and then reports
**every observation `UNAVAILABLE` forever**, which cannot prove that reads return real values or
that the `/sample` long poll delivers anything. `adapter.py` is what makes the fixture live.
The Agent dials **out** to the adapter (see the `Adapters` block in `agent.cfg`); the adapter is
not published to the host.
## Run
From the shared Docker host (stack dir `/opt/otopcua-mtconnect`):
```bash
docker compose up -d --wait
docker compose logs -f agent
docker compose down
```
From a dev box via the helper (see CLAUDE.md "Docker Workflow"):
```powershell
lmxopcua-fix sync mtconnect # push this folder to /opt/otopcua-mtconnect/
lmxopcua-fix up mtconnect # single-service-shaped stack, no profile argument
lmxopcua-fix logs mtconnect
lmxopcua-fix down mtconnect
```
### Running it on a Mac
macOS **squats port 5000** — AirPlay Receiver (ControlCenter) binds `*:5000` and wins the race,
so `docker port` reports a healthy publish while every request answers `403 Forbidden` with
`Server: AirTunes`. Use the host-port override:
```bash
MTCONNECT_AGENT_HOST_PORT=5555 docker compose up -d --wait
MTCONNECT_AGENT_ENDPOINT=http://127.0.0.1:5555 dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests
```
## Endpoint
- Default: `http://10.100.0.35:5000` (the shared Docker host; 5000 is the Agent's own default port).
- Override with `MTCONNECT_AGENT_ENDPOINT` to point at a real Agent on a machine tool.
- `MTCONNECT_AGENT_HOST_PORT` changes only the **published host port** of the fixture container.
`MTConnectAgentFixture` issues one `GET {endpoint}/probe` at collection init and records a
`SkipReason` when it fails, so the suite skips cleanly on a box with no fixture running.
## The seeded device model
Every named DataItem deliberately has `name != id` — the inverse of the driver's hand-authored
unit fixtures, and the only arrangement under which confusing the browse name with the
observation correlation key is visible.
| DataItem `id` | `name` | Category | Type | Repr. | Inferred type |
|---|---|---|---|---|---|
| `fixture_avail` | `Favail` | EVENT | AVAILABILITY | | String |
| `fixture_x_pos` | `Xact` | SAMPLE | POSITION (ACTUAL, MILLIMETER) | | Float64 — **moves** |
| `fixture_x_load` | `Xload` | SAMPLE | LOAD (PERCENT) | | Float64 — **moves** |
| `fixture_x_travel` | `Xtravel` | CONDITION | POSITION | | String + IsAlarm |
| `fixture_c_speed` | `Cspeed` | SAMPLE | ROTARY_VELOCITY (REVOLUTION/MINUTE) | | Float64 — **moves** |
| `fixture_c_temp_series` | `Ctemps` | SAMPLE | TEMPERATURE (CELSIUS) | TIME_SERIES | Float64 **array**, ArrayDim `null` |
| `fixture_mode` | `Cmode` | EVENT | CONTROLLER_MODE | | String |
| `fixture_execution` | `Pexec` | EVENT | EXECUTION | | String |
| `fixture_partcount` | `Pcount` | EVENT | **PART_COUNT** | | **Int64** — the regression case |
| `fixture_linenumber` | `Pline` | EVENT | LINE_NUMBER | | Int64 |
| `fixture_program` | `Pprogram` | EVENT | PROGRAM | | String |
| `fixture_block` | `Pblock` | EVENT | BLOCK | | String — **never fed ⇒ UNAVAILABLE** |
| `fixture_varset` | `Pvars` | EVENT | VARIABLE | DATA_SET | String (structured ⇒ BadNotSupported) |
| `fixture_logic` | `Plogic` | CONDITION | LOGIC_PROGRAM | | String + IsAlarm |
| `fixture_asset_changed` / `fixture_asset_removed` | — | EVENT | ASSET_CHANGED / ASSET_REMOVED | | String |
The Agent additionally injects **its own `<Agent>` self-model device** (connection status,
observation update rate, adapter URI). That is normal for every MTConnect 2.x Agent and the test
suite excludes it from value-plane assertions — its update-rate samples tick whether or not any
adapter is attached, so including them would let "the stream delivered a changed value" pass
against an Agent with no data source at all.
## Two things a real Agent taught us (both cost a fixture restart to find)
1. **`agent.cfg` must be pure ASCII.** A single non-ASCII byte — even inside a comment — makes
the config parser reject the whole file with a bare `Failed / Stopped at line: N` and exit.
2. **`sampleCount` is not a DataItem attribute.** It belongs to a TIME_SERIES *observation*. An
Agent that meets it on a declaration logs
`The following keys were present and not expected: sampleCount` followed by
`DataItems: Invalid element 'DataItem'` and **drops the entire data item** from the device
model. `MTConnectDataTypeInference` therefore only ever sees `sampleCount = null` from a real
Agent, so a live TIME_SERIES tag is always a variable-length array.