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

5.6 KiB

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 Two services: agent (published on :5000) and adapter (internal, :7878)
agent.cfg Agent configuration, bind-mounted at /mtconnect/config/agent.cfg (the image's CMD path)
Devices.xml The canned device model the Agent serves from /probe
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):

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"):

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:

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.