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.
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, notmtconnect/cppagent.cppagentis 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_ENDPOINTto point at a real Agent on a machine tool. MTCONNECT_AGENT_HOST_PORTchanges 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)
agent.cfgmust be pure ASCII. A single non-ASCII byte — even inside a comment — makes the config parser reject the whole file with a bareFailed / Stopped at line: Nand exit.sampleCountis not a DataItem attribute. It belongs to a TIME_SERIES observation. An Agent that meets it on a declaration logsThe following keys were present and not expected: sampleCountfollowed byDataItems: Invalid element 'DataItem'and drops the entire data item from the device model.MTConnectDataTypeInferencetherefore only ever seessampleCount = nullfrom a real Agent, so a live TIME_SERIES tag is always a variable-length array.