Skip to content

Testing in CI

Ralf Steinert edited this page Nov 9, 2026 · 3 revisions

Two ways to get simulated devices into automated tests: the pytest plugin, or hart-sim serve as a background process for any language and test runner.

pytest plugin

pip install hart-sim registers two fixtures. They start a new bench on free ports for every test and stop it afterwards, so tests cannot leak device state into each other.

import pytest


def test_reads_the_transmitter(hart_bench):
    with hart_bench.client() as client:                 # a connected HartIPClient
        variables = client.handle(0).read_variables()
    assert variables.pv_unit == 12                      # kPa
    assert 3.8 <= variables.current_ma <= 20.5


def test_alarm_current(hart_bench):
    hart_bench.devices["PT-101"].faults.inject("sensor_failure")
    with hart_bench.client() as client:
        assert client.handle(0).read_variables().current_ma == pytest.approx(21.0)

hart_bench is one pressure device at polling address 0 with tag PT-101, unless you choose a bench with --hart-bench FILE or the ini key hart_bench. For more devices, use the factory:

def test_two_devices(hart_bench_factory):
    bench = hart_bench_factory({"devices": [
        {"profile": "pressure", "polling_address": 1, "device_id": 1, "tag": "PT-101"},
        {"profile": "temperature", "polling_address": 2, "device_id": 2, "tag": "TT-201"},
    ]})
    with bench.client() as client:
        assert client.find("TT-201").read_variables().pv_unit == 32     # degC

The factory takes a bench file, a dict or a bundled profile name. bench.client(transport="udp", timeout=0.5) sets the transport and the timeout.

GitHub Actions

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-python@v7
        with:
          python-version: "3.13"
      - run: pip install hart-sim pytest
      - run: pytest

As a service container

For a job that tests something other than Python, run the published image next to it. GitHub Actions waits until the image reports healthy, so no sleep is needed. Pin the version.

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      hart:
        image: ghcr.io/steitec/hart-sim:0.1.2
        ports:
          - 5094:5094                # HART-IP over TCP
          - 5094:5094/udp            # HART-IP over UDP
          - 127.0.0.1:5095:5095      # control channel, no authentication
    steps:
      - uses: actions/checkout@v7
      - run: ./run-my-system-tests   # connects to 127.0.0.1:5094

The default bench has PT-101 at polling address 1 and TT-201 at polling address 2. If the job itself runs in a container, leave out ports and connect to the host name hart instead. Service containers start before actions/checkout, so they cannot mount a bench file from your repository; to use your own, start the image with docker run -d -v "$PWD/bench.yaml:/bench/bench.yaml" ... in a step after the checkout.

As a background process

For tests in another language, or a system test against a built application:

pip install hart-sim
hart-sim serve --bench plant.yaml &
sleep 1
./run-my-system-tests                     # connects to 127.0.0.1:5094
hart-sim inject PT-101 sensor_failure     # from a script, or the JSON below

The control channel takes one JSON object per line on port 5095, so any language can inject faults:

echo '{"device": "PT-101", "inject": "sensor_failure", "params": {}}' | nc -q1 127.0.0.1 5095
{"ok": true}

Neither HART-IP nor the control channel has authentication. Both listen on loopback by default.

Clone this wiki locally