A Docker-based interoperability testing framework for Sparkplug B 3.0.0, providing independent edge node and host application implementations, deterministic fixtures, and automated cross-stack testing on four MQTT brokers. Test Sparkplug B libraries against established implementations including Eclipse Tahu (Java and JavaScript), pysparkplug, SparkplugNet, srad and sparkplughost. Reference implementations are independently containerized, so third-party stacks never have to be integrated into the library under test.
| Adapter | Implementation | Language | Edge | Host | Image |
|---|---|---|---|---|---|
tahu |
Eclipse Tahu 1.0.21 | Java | yes | yes | ghcr.io/otfabric/sparkplugb-interop-tahu |
pysparkplug |
pysparkplug 0.6.1 | Python | yes | yes | ghcr.io/otfabric/sparkplugb-interop-pysparkplug |
sparkplugnet |
SparkplugNet 1.4.1 | C# | yes | yes | ghcr.io/otfabric/sparkplugb-interop-sparkplugnet |
srad |
srad 0.5.0 | Rust | yes | yes | ghcr.io/otfabric/sparkplugb-interop-srad |
tahu-js |
Eclipse Tahu sparkplug-client 3.2.4 | JavaScript | yes | no | ghcr.io/otfabric/sparkplugb-interop-tahu-js |
sparkplughost |
EvergenEnergy/sparkplughost v0.4.0 | Go | no | yes | ghcr.io/otfabric/sparkplugb-interop-sparkplughost |
| Broker | Version | Image (pinned by digest in versions.yaml) |
|---|---|---|
| Eclipse Mosquitto | 2.1.2 | eclipse-mosquitto:2.1.2-alpine |
| HiveMQ Community Edition | 2026.5 | hivemq/hivemq-ce:2026.5 |
| EMQX | 6.3.2 | emqx/emqx:6.3.2 |
| NanoMQ | 0.25.7 | emqx/nanomq:0.25.7 |
The stacks do not all do the same: only Tahu (Java) waits for a primary host,
Tahu and tahu-js rebirth on Node Control/Rebirth, pysparkplug and
SparkplugNet edges publish data by name rather than alias, and SparkplugNet's
host neither resolves aliases nor checks seq. Each image says what it
supports (print-capabilities); see docs/CAPABILITIES.md.
Evaluated and left out: Tahu C and Node-RED. See PLAN.md.
Docs: docs/CONTAINER_CONTRACT.md (commands, events, call results, exit codes) · docs/FIXTURES.md (the simulated edge nodes) · docs/CAPABILITIES.md · docs/COMPATIBILITY.md (pins, cross-stack matrix, findings) · REQUIREMENTS.md · PLAN.md · RELEASE.md
It owns the reference implementations, their adapters, the fixtures, the container contract, the images and the self-tests that show the references agree with each other on every broker.
It does not contain, import, build or know about any library under test. otfabric/go-sparkplugb is a consumer: it pulls the images and owns its own scenarios, assertions and compatibility claims. A passing self-test here says the references interoperate; it says nothing about a consumer.
The fixtures describe what an edge node is; the adapters make each implementation behave that way; the consumer decides what to assert.
Needs Docker and jq.
make images # build the adapter images for this machine
make smoke # container contract of each image
make interop # every host against every edge, every fixture, on Mosquitto
make interop BROKER=nanomqAn edge node and a host application, on one broker:
docker network create sp
docker run -d --name broker --network sp eclipse-mosquitto:2.1.2-alpine mosquitto -c /mosquitto-no-auth.conf
docker run -d --name host --network sp ghcr.io/otfabric/sparkplugb-interop-srad:dev host --broker tcp://broker --host-id h1
docker run -d --name edge --network sp ghcr.io/otfabric/sparkplugb-interop-tahu:dev edge --broker tcp://broker
docker exec edge sparkplugb-interop call publish-data --payload '{"update": 0}'
docker exec host sparkplugb-interop call nodes | jq .
docker logs host | jq -c 'select(.event == "message") | {type, edgeNode, device, seq}'Every image has the same commands:
edge a Sparkplug edge node with the metrics and devices of a fixture
host a primary Sparkplug host application
call <operation> drive a running edge or host: publish-data, rebirth, device-death,
device-birth, send-command, request-rebirth, nodes
print-capabilities what this adapter supports, as JSON
print-fixture the fixture baked into the image
edge and host write what they publish and receive to stdout as JSON
Lines; call returns a result document. Both are specified by JSON Schemas
in schemas/.
make documents also writes reports/results.json, the organisation's
interop-result-v1. Only pass counts as passing: a run with
no pass, or with a known bug that no longer reproduces, fails.
A consumer needs Docker, the image references and its own test code. It does not clone this repository.
Library as host, reference as edge. Start a broker and the library's host, then the reference edge, and wait for its readiness file (never for a delay):
docker run -d --name edge --network sp ghcr.io/otfabric/sparkplugb-interop-tahu:<tag> edge --broker tcp://broker --fixture scalars
until docker exec edge test -f /run/sparkplugb-interop/ready; do sleep 0.1; done
docker exec edge sparkplugb-interop call publish-data --payload '{"update": 0}'The edge's stdout says what it published (published) and which commands it
applied (command): assert that the library's view of the node matches.
Library as edge, reference as host. Start the reference host, connect the
library's edge node, and read what the host saw: its message events, and
call nodes for its view of every node, device and metric. call send-command and call request-rebirth make the host command the library.
Expected values come from the image: print-fixture scalars prints the
fixture it was built with, so expectations cannot drift from the image.
What a stack supports comes from the image too: print-capabilities.
Skip, do not fail, a scenario that needs a feature or a datatype an image
does not declare.
Pin by digest in release qualification
(ghcr.io/otfabric/sparkplugb-interop-tahu@sha256:...); the digests are in
each release's manifest.json and its notes.
sparkplugb-interop/
├── adapters/
│ ├── tahu/ Java adapter: Dockerfile, pom.xml, src/
│ ├── pysparkplug/ Python adapter: Dockerfile, requirements (hashed), adapter/
│ ├── sparkplugnet/ C# adapter: Dockerfile, adapter/
│ ├── srad/ Rust adapter: Dockerfile, Cargo.toml, src/
│ ├── tahu-js/ JavaScript adapter: Dockerfile, package.json, adapter/
│ └── sparkplughost/ Go adapter: Dockerfile, go.mod, main.go, overlay/
├── fixtures/
│ ├── schema/ JSON Schema of the fixture format
│ ├── baseline/ an edge node with two devices and writable metrics
│ └── scalars/ an edge node with every scalar datatype
├── schemas/ JSON Schemas of capabilities, events and call results
├── tests/known-bugs.tsv bugs of upstream versions, scoped by role, peer, broker, fixture and step
├── scripts/ validate, smoke, cross-stack self-test, upstream candidates, release manifest
├── docs/ contract, fixtures, capabilities, compatibility
├── versions.yaml every upstream pin, broker and base image digest
└── Makefile
make help # all targets
make ci # validate, lint, build, smoke, cross-stack self-test, document schemas
make validate # fixtures, versions.yaml and RELEASE.md
make interop-brokers # the cross-stack self-test on every broker
make buildx # check the other architecture locally, under emulation (CI builds both natively)
make upstream # pin vs latest release of every adapter library and broker
make candidate-srad CANDIDATE=0.6.0 # self-test an adapter on another upstream version (pins untouched)
make candidate-nanomq CANDIDATE=emqx/nanomq:0.25.8 # the pinned adapters on another broker releaseThe Upstream workflow (Actions → Upstream → Run workflow; also daily)
resolves the latest release of Eclipse Tahu, pysparkplug, SparkplugNet, srad,
the Tahu sparkplug-client, sparkplughost and the four brokers, and the digest
of every base image. It self-tests every release that is newer than its pin
(once per pin and release), reports pin versus latest in its job summary and
keeps the issue "Upstream releases: pins behind the latest" open while a pin
is behind. Its force input self-tests even when the pin is the latest;
only limits it to some names (and leaves the issue alone).
make smoke ADAPTERS=srad and make interop ADAPTERS="tahu srad" narrow what
is run. See CONTRIBUTING.md for adding an adapter, a
fixture or a call operation.
The brokers accept anonymous clients and the edge nodes apply writes from anyone who can publish to them. They are test fixtures: the self-tests put them on a private Docker network, and they must never be attached to a plant network. See SECURITY.md.
The repository's code and documentation, the adapters included, are MIT-licensed. The images also contain the upstream stacks and their dependencies, each under its own licence (EPL-2.0, Apache-2.0, MIT and others). See LICENSE and THIRD_PARTY_NOTICES.md.