From 3435cecb9cf76856e4f3cc32a9ebd83cea70c245 Mon Sep 17 00:00:00 2001 From: Travis Collins Date: Thu, 14 May 2026 00:53:33 +0000 Subject: [PATCH 01/24] feat(ci): GHA hardware-test workflow alongside JenkinsfileHW Net-new GH Actions HW pipeline calling the shared hw-matrix reusable workflow in tfcollins/labgrid-plugins. Runs alongside the existing JenkinsfileHW for at least two weeks during the migration period; do not delete the Jenkins config until result delta is understood. Initial scope: 2 places (pluto, fmcomms2-zcu102), coord leg only, labeled-PR + nightly cron + manual triggers. Add boards by extending .github/hw-nodes.json and writing test/hw/test__smoke.py. New artifacts: - .github/workflows/hardware-test.yml: thin caller of tfcollins/labgrid-plugins/.github/workflows/hw-matrix.yml@v1. - .github/hw-nodes.json: per-place manifest consumed by hw-matrix. - test/hw/conftest.py: 'iio_uri' session fixture that resolves a libiio URI from either --iio-uri-override (laptop runs) or LG_COORDINATOR + LG_PLACE (set by the workflow before pytest). - test/hw/test_pluto_smoke.py, test/hw/test_ad9361_smoke.py: minimal smoke tests that open a context and capture a buffer. Independent of test/conftest.py and its pytest-libiio fixtures. - test/hw/env/{pluto,fmcomms2_zcu102}.yaml: coord-mode env stubs. - test/hw/README.md: onboarding for adding boards. The existing JenkinsfileHW and test/test_*.py are untouched. --- .github/hw-nodes.json | 16 +++++ .github/workflows/hardware-test.yml | 47 +++++++++++++++ .gitignore | 1 + test/hw/README.md | 65 ++++++++++++++++++++ test/hw/conftest.py | 94 +++++++++++++++++++++++++++++ test/hw/env/fmcomms2_zcu102.yaml | 7 +++ test/hw/env/pluto.yaml | 12 ++++ test/hw/test_ad9361_smoke.py | 34 +++++++++++ test/hw/test_pluto_smoke.py | 36 +++++++++++ 9 files changed, 312 insertions(+) create mode 100644 .github/hw-nodes.json create mode 100644 .github/workflows/hardware-test.yml create mode 100644 test/hw/README.md create mode 100644 test/hw/conftest.py create mode 100644 test/hw/env/fmcomms2_zcu102.yaml create mode 100644 test/hw/env/pluto.yaml create mode 100644 test/hw/test_ad9361_smoke.py create mode 100644 test/hw/test_pluto_smoke.py diff --git a/.github/hw-nodes.json b/.github/hw-nodes.json new file mode 100644 index 0000000000..3409aa4616 --- /dev/null +++ b/.github/hw-nodes.json @@ -0,0 +1,16 @@ +[ + { + "place": "pluto", + "runner_label": "lab-pluto", + "env_remote": "test/hw/env/pluto.yaml", + "tests": ["test/hw/test_pluto_smoke.py"], + "legs": ["coord"] + }, + { + "place": "fmcomms2-zcu102", + "runner_label": "lab-fmcomms2", + "env_remote": "test/hw/env/fmcomms2_zcu102.yaml", + "tests": ["test/hw/test_ad9361_smoke.py"], + "legs": ["coord"] + } +] diff --git a/.github/workflows/hardware-test.yml b/.github/workflows/hardware-test.yml new file mode 100644 index 0000000000..50be3349b2 --- /dev/null +++ b/.github/workflows/hardware-test.yml @@ -0,0 +1,47 @@ +name: Hardware Tests (GHA) + +# Net-new GHA HW pipeline running alongside JenkinsfileHW during migration. +# Calls the shared HW-CI workflow in tfcollins/labgrid-plugins. +# +# Triggered by: +# * workflow_dispatch (manual) +# * PRs labeled `hw-test` — keeps every push from burning lab time +# * Nightly cron at 08:00 UTC +# +# Initial scope: `pluto` and `fmcomms2-zcu102` places (coord leg only). +# Add boards by extending .github/hw-nodes.json and test/hw/. + +permissions: + contents: read + checks: write + pull-requests: write + +on: + workflow_dispatch: + pull_request: + types: [labeled, opened, synchronize, reopened] + schedule: + - cron: "0 8 * * *" + +jobs: + hw: + if: >- + github.event_name != 'pull_request' || + contains(github.event.pull_request.labels.*.name, 'hw-test') + uses: tfcollins/labgrid-plugins/.github/workflows/hw-matrix.yml@v1 + with: + manifest_path: .github/hw-nodes.json + venv_install_cmd: | + uv pip install --quiet --python "$VENV_DIR/bin/python" \ + -r requirements_dev.txt + uv pip install --quiet --python "$VENV_DIR/bin/python" -e . + uv pip install --quiet --python "$VENV_DIR/bin/python" \ + "labgrid @ git+https://github.com/tfcollins/labgrid.git@81c35eda6436af63d3acf7f8b98893abae0bbbca" + pytest_cmd_template: >- + "$VENV_DIR/bin/pytest" -v $TESTS --junitxml="$JUNIT" + artifact_glob: | + junit-hw-*.xml + **/*.log + legs: coord + prism_project: pyadi-iio + secrets: inherit diff --git a/.gitignore b/.gitignore index dee43f70bb..6bf5e0c356 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,7 @@ celerybeat-schedule .env .venv env/ +!test/hw/env/ venv/ ENV/ env.bak/ diff --git a/test/hw/README.md b/test/hw/README.md new file mode 100644 index 0000000000..5e29e84aed --- /dev/null +++ b/test/hw/README.md @@ -0,0 +1,65 @@ +# HW smoke tests (`test/hw/`) + +Minimal pyadi-iio tests that run against real boards held by a labgrid +coordinator. Wired into CI by `.github/workflows/hardware-test.yml`, +which calls the shared `hw-matrix` reusable workflow in +`tfcollins/labgrid-plugins`. + +This suite is **independent** of the historic `test/test_*.py` files +under `test/` (which use the `pytest-libiio` plugin and a host_map). It +exists so the new GH Actions HW pipeline has something to run while the +existing suite stays on Jenkins during the migration. + +## Layout + +``` +test/hw/ +├── conftest.py # iio_uri fixture: labgrid place → ip:URI +├── env/ +│ ├── pluto.yaml # coord-mode env for the 'pluto' place +│ └── fmcomms2_zcu102.yaml +├── test_pluto_smoke.py # smoke for adi.Pluto +└── test_ad9361_smoke.py # smoke for adi.ad9361 (FMComms2/3 carrier) +``` + +## How a test resolves a URI + +`conftest.py::iio_uri` (session-scoped) returns an `ip:...` URI by: + +1. **`--iio-uri-override `** or **`IIO_URI_OVERRIDE` env var** — bypass + labgrid entirely (for laptop runs against a known-good DUT). +2. **`LG_COORDINATOR` + `LG_PLACE`** — set by the HW workflow. The + workflow's acquire-place composite has already acquired the place + before pytest starts; conftest runs `labgrid-client show` and parses + the first `address:` / `host:` / `ipaddr:` line under the held + `NetworkService` resource. + +If neither source is set, every test under `test/hw/` is skipped. + +## Running locally + +```bash +# Without labgrid (point at an iiod yourself) +pytest -v test/hw/test_pluto_smoke.py --iio-uri-override ip:192.168.2.1 + +# Against the lab coordinator +LG_COORDINATOR=10.0.0.41:20408 LG_PLACE=pluto \ + pytest -v test/hw/test_pluto_smoke.py +``` + +## Adding a new board + +1. Add an entry to `.github/hw-nodes.json` (place + runner_label + the + test files to run for that place; `legs: coord` for now). +2. Create `test/hw/env/.yaml` mirroring the structure of the + existing files — only the `RemotePlace.name` needs to change. +3. Write `test/hw/test__smoke.py`. Take the `iio_uri` fixture, + instantiate the right `adi.(uri=iio_uri)`, exercise one or + two attributes plus a buffer. Don't reuse the heavyweight fixtures + from `test/conftest.py`. + +## Running alongside Jenkins + +The existing `JenkinsfileHW` is unchanged. Both pipelines run on every +PR for at least 2 weeks; if results diverge, open a tracking issue +before disabling either one. diff --git a/test/hw/conftest.py b/test/hw/conftest.py new file mode 100644 index 0000000000..bf7dee85df --- /dev/null +++ b/test/hw/conftest.py @@ -0,0 +1,94 @@ +"""HW-test conftest. + +Bridges the labgrid place held by the calling workflow (or a manually-set +LG_PLACE / LG_COORDINATOR pair) into a libiio URI usable by ``adi`` +device classes. + +Used by the new GH Actions hardware-test workflow (which calls +labgrid-plugins' hw-matrix reusable workflow). Tests under ``test/hw/`` +take an ``iio_uri`` fixture and instantiate the device class with it. + +When LG_ENV / LG_PLACE / LG_COORDINATOR are unset, the suite falls back +to a single ``--iio-uri-override`` CLI option (or the IIO_URI_OVERRIDE +env var) so local manual runs without labgrid still work. + +Sibling test/conftest.py (the historic libiio-plugin-driven suite under +test/) is untouched; this conftest only applies to tests collected +under test/hw/. +""" + +from __future__ import annotations + +import os +import subprocess +import sys + +import pytest + + +def pytest_addoption(parser): + g = parser.getgroup("hw") + g.addoption( + "--iio-uri-override", + default=os.environ.get("IIO_URI_OVERRIDE"), + help="Bypass labgrid; libiio URI to point tests at (e.g. ip:10.0.0.132).", + ) + + +def _labgrid_show(coord: str, place: str): + """Run `python -m labgrid.remote.client -p PLACE show` and return stdout.""" + return subprocess.run( + [sys.executable, "-m", "labgrid.remote.client", "-x", coord, "-p", place, "show"], + capture_output=True, + text=True, + check=False, + timeout=15, + ) + + +@pytest.fixture(scope="session") +def iio_uri(request) -> str: + """Return an `ip:...` URI to a real-iiod we can talk to. + + Resolution order: + 1. ``--iio-uri-override `` or ``IIO_URI_OVERRIDE`` env var + 2. The labgrid place named by ``LG_PLACE`` on ``LG_COORDINATOR`` + (set by the reusable HW workflow before pytest runs; the + place is already acquired at workflow level). + """ + override = request.config.getoption("--iio-uri-override") + if override: + return override + + coord = os.environ.get("LG_COORDINATOR") + place = os.environ.get("LG_PLACE") + if not (coord and place): + pytest.skip( + "no IIO URI source: set --iio-uri-override, or run under the " + "HW workflow which exports LG_COORDINATOR + LG_PLACE" + ) + + try: + import labgrid # noqa: F401 + except ImportError: + pytest.skip("labgrid not importable; pass --iio-uri-override instead") + + show = _labgrid_show(coord, place) + if show.returncode != 0 or "Place" not in show.stdout: + pytest.skip( + f"labgrid place {place!r} unavailable at {coord}: " + f"{show.stdout.strip()} {show.stderr.strip()}" + ) + + address = None + for line in show.stdout.splitlines(): + s = line.strip() + if s.startswith(("address:", "host:", "ipaddr:")): + address = s.split(":", 1)[1].strip() + break + if not address: + pytest.skip( + f"labgrid place {place!r} has no NetworkService address — " + "verify the exporter publishes one" + ) + return f"ip:{address}" diff --git a/test/hw/env/fmcomms2_zcu102.yaml b/test/hw/env/fmcomms2_zcu102.yaml new file mode 100644 index 0000000000..03e782af56 --- /dev/null +++ b/test/hw/env/fmcomms2_zcu102.yaml @@ -0,0 +1,7 @@ +# Coordinator-mode labgrid env for the 'fmcomms2-zcu102' place. +# See test/hw/env/pluto.yaml for the contract. +targets: + main: + resources: + RemotePlace: + name: fmcomms2-zcu102 diff --git a/test/hw/env/pluto.yaml b/test/hw/env/pluto.yaml new file mode 100644 index 0000000000..7b445b46ed --- /dev/null +++ b/test/hw/env/pluto.yaml @@ -0,0 +1,12 @@ +# Coordinator-mode labgrid env for the 'pluto' place. +# +# Consumed by the HW workflow (hw-matrix) via LG_ENV; the workflow itself +# only relies on the place name (LG_PLACE) and coordinator (LG_COORDINATOR) +# being exported, and on the acquire-place composite holding the place +# before pytest starts. test/hw/conftest.py extracts the libiio URI from +# the held place at session scope. +targets: + main: + resources: + RemotePlace: + name: pluto diff --git a/test/hw/test_ad9361_smoke.py b/test/hw/test_ad9361_smoke.py new file mode 100644 index 0000000000..ee343702e9 --- /dev/null +++ b/test/hw/test_ad9361_smoke.py @@ -0,0 +1,34 @@ +"""Smoke test: open a libiio context against an AD9361-based board (FMComms2/3). + +Run under the new GH Actions HW workflow with LG_COORDINATOR + LG_PLACE +set (see conftest.py), or locally via: + + pytest -v test/hw/test_ad9361_smoke.py --iio-uri-override ip:10.0.0.50 +""" + +from __future__ import annotations + +import pytest + +import adi + + +def test_open_context(iio_uri): + """ad9361 driver attaches to the iio context and exposes attrs.""" + sdr = adi.ad9361(uri=iio_uri) + try: + assert sdr.ctx is not None + finally: + del sdr + + +def test_rx_buffer(iio_uri): + """Capture a single RX buffer end-to-end.""" + sdr = adi.ad9361(uri=iio_uri) + try: + sdr.rx_buffer_size = 1024 + data = sdr.rx() + assert data is not None + assert len(data) > 0 + finally: + del sdr diff --git a/test/hw/test_pluto_smoke.py b/test/hw/test_pluto_smoke.py new file mode 100644 index 0000000000..a327c9d880 --- /dev/null +++ b/test/hw/test_pluto_smoke.py @@ -0,0 +1,36 @@ +"""Smoke test: open a libiio context against a PlutoSDR and read its product ID. + +Run under the new GH Actions HW workflow with LG_COORDINATOR + LG_PLACE +set (see conftest.py), or locally via: + + pytest -v test/hw/test_pluto_smoke.py --iio-uri-override ip:192.168.2.1 +""" + +from __future__ import annotations + +import pytest + +import adi + + +def test_open_context(iio_uri): + """Pluto answers ctx.attrs and exposes its product ID.""" + sdr = adi.Pluto(uri=iio_uri) + try: + # `product_id` lives in libiio context attrs; raise if it's missing. + ctx_attrs = dict(sdr.ctx.attrs) + assert ctx_attrs, f"context attrs empty for {iio_uri}" + finally: + del sdr + + +def test_rx_buffer(iio_uri): + """Capture a single RX buffer end-to-end.""" + sdr = adi.Pluto(uri=iio_uri) + try: + sdr.rx_buffer_size = 1024 + data = sdr.rx() + assert data is not None + assert len(data) == sdr.rx_buffer_size + finally: + del sdr From 3c3aec2818724e11b761b35c774384c76de5c8d0 Mon Sep 17 00:00:00 2001 From: Travis Collins Date: Thu, 14 May 2026 01:33:36 +0000 Subject: [PATCH 02/24] fix(test/hw): satisfy black 19.10b0 line-split in conftest The subprocess argument list in _labgrid_show was a single 96-char line; the repo's pinned black==19.10b0 (via pre-commit) splits long literal lists one element per line. No behavior change. --- test/hw/conftest.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/hw/conftest.py b/test/hw/conftest.py index bf7dee85df..7ad7764e25 100644 --- a/test/hw/conftest.py +++ b/test/hw/conftest.py @@ -38,7 +38,16 @@ def pytest_addoption(parser): def _labgrid_show(coord: str, place: str): """Run `python -m labgrid.remote.client -p PLACE show` and return stdout.""" return subprocess.run( - [sys.executable, "-m", "labgrid.remote.client", "-x", coord, "-p", place, "show"], + [ + sys.executable, + "-m", + "labgrid.remote.client", + "-x", + coord, + "-p", + place, + "show", + ], capture_output=True, text=True, check=False, From e45fe52173490ffd696c98e53a6b302ac37d2af3 Mon Sep 17 00:00:00 2001 From: Travis Collins Date: Thu, 14 May 2026 02:14:55 +0000 Subject: [PATCH 03/24] fix(hw): wire smoke tests to real coordinator places MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace placeholder pluto / fmcomms2-zcu102 manifest entries (which don't exist on coordinator 10.0.0.41) with the actual hardware behind the lab: - mini2 (ZCU102 + AD9081) -> adi.ad9081 smoke - bq (ZC706 + ADRV9371) -> adi.ad9371 smoke - nemo (ZC706 + ADRV9009) -> adi.adrv9009 smoke Each smoke instantiates the device class via the iio_uri fixture (which extracts the labgrid place's NetworkService address) and runs two assertions: - ctx.attrs is populated (real libiio context handshake) - rx() returns a non-empty buffer (real DMA roundtrip) No fmcdaq3 class in adi/ yet, so nuc/fmcdaq3 is deferred. Tests deliberately don't .skip() on connection failure — if a DUT is offline the test FAILS (which is the right signal, not silent). --- .github/hw-nodes.json | 23 +++++++++++++------- test/hw/env/bq.yaml | 6 ++++++ test/hw/env/fmcomms2_zcu102.yaml | 7 ------- test/hw/env/mini2.yaml | 8 +++++++ test/hw/env/nemo.yaml | 6 ++++++ test/hw/env/pluto.yaml | 12 ----------- test/hw/test_ad9081_smoke.py | 32 ++++++++++++++++++++++++++++ test/hw/test_ad9361_smoke.py | 34 ------------------------------ test/hw/test_ad9371_smoke.py | 31 +++++++++++++++++++++++++++ test/hw/test_adrv9009_smoke.py | 31 +++++++++++++++++++++++++++ test/hw/test_pluto_smoke.py | 36 -------------------------------- 11 files changed, 129 insertions(+), 97 deletions(-) create mode 100644 test/hw/env/bq.yaml delete mode 100644 test/hw/env/fmcomms2_zcu102.yaml create mode 100644 test/hw/env/mini2.yaml create mode 100644 test/hw/env/nemo.yaml delete mode 100644 test/hw/env/pluto.yaml create mode 100644 test/hw/test_ad9081_smoke.py delete mode 100644 test/hw/test_ad9361_smoke.py create mode 100644 test/hw/test_ad9371_smoke.py create mode 100644 test/hw/test_adrv9009_smoke.py delete mode 100644 test/hw/test_pluto_smoke.py diff --git a/.github/hw-nodes.json b/.github/hw-nodes.json index 3409aa4616..57616c6855 100644 --- a/.github/hw-nodes.json +++ b/.github/hw-nodes.json @@ -1,16 +1,23 @@ [ { - "place": "pluto", - "runner_label": "lab-pluto", - "env_remote": "test/hw/env/pluto.yaml", - "tests": ["test/hw/test_pluto_smoke.py"], + "place": "mini2", + "runner_label": "hw-mini2", + "env_remote": "test/hw/env/mini2.yaml", + "tests": ["test/hw/test_ad9081_smoke.py"], "legs": ["coord"] }, { - "place": "fmcomms2-zcu102", - "runner_label": "lab-fmcomms2", - "env_remote": "test/hw/env/fmcomms2_zcu102.yaml", - "tests": ["test/hw/test_ad9361_smoke.py"], + "place": "bq", + "runner_label": "hw-bq", + "env_remote": "test/hw/env/bq.yaml", + "tests": ["test/hw/test_ad9371_smoke.py"], + "legs": ["coord"] + }, + { + "place": "nemo", + "runner_label": "hw-nemo", + "env_remote": "test/hw/env/nemo.yaml", + "tests": ["test/hw/test_adrv9009_smoke.py"], "legs": ["coord"] } ] diff --git a/test/hw/env/bq.yaml b/test/hw/env/bq.yaml new file mode 100644 index 0000000000..d93729f8ca --- /dev/null +++ b/test/hw/env/bq.yaml @@ -0,0 +1,6 @@ +# Coord-mode env for bq (ZC706 + ADRV9371). See mini2.yaml for the pattern. +targets: + main: + resources: + RemotePlace: + name: bq diff --git a/test/hw/env/fmcomms2_zcu102.yaml b/test/hw/env/fmcomms2_zcu102.yaml deleted file mode 100644 index 03e782af56..0000000000 --- a/test/hw/env/fmcomms2_zcu102.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# Coordinator-mode labgrid env for the 'fmcomms2-zcu102' place. -# See test/hw/env/pluto.yaml for the contract. -targets: - main: - resources: - RemotePlace: - name: fmcomms2-zcu102 diff --git a/test/hw/env/mini2.yaml b/test/hw/env/mini2.yaml new file mode 100644 index 0000000000..7d4206b3c7 --- /dev/null +++ b/test/hw/env/mini2.yaml @@ -0,0 +1,8 @@ +# Coord-mode env for mini2 (ZCU102 + AD9081). Minimal: just RemotePlace so +# labgrid resolves the place; the conftest reads LG_PLACE / LG_COORDINATOR +# directly to extract the IIO URI without invoking labgrid drivers. +targets: + main: + resources: + RemotePlace: + name: mini2 diff --git a/test/hw/env/nemo.yaml b/test/hw/env/nemo.yaml new file mode 100644 index 0000000000..e1809efb74 --- /dev/null +++ b/test/hw/env/nemo.yaml @@ -0,0 +1,6 @@ +# Coord-mode env for nemo (ZC706 + ADRV9009). See mini2.yaml for the pattern. +targets: + main: + resources: + RemotePlace: + name: nemo diff --git a/test/hw/env/pluto.yaml b/test/hw/env/pluto.yaml deleted file mode 100644 index 7b445b46ed..0000000000 --- a/test/hw/env/pluto.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Coordinator-mode labgrid env for the 'pluto' place. -# -# Consumed by the HW workflow (hw-matrix) via LG_ENV; the workflow itself -# only relies on the place name (LG_PLACE) and coordinator (LG_COORDINATOR) -# being exported, and on the acquire-place composite holding the place -# before pytest starts. test/hw/conftest.py extracts the libiio URI from -# the held place at session scope. -targets: - main: - resources: - RemotePlace: - name: pluto diff --git a/test/hw/test_ad9081_smoke.py b/test/hw/test_ad9081_smoke.py new file mode 100644 index 0000000000..4e86ea7c4c --- /dev/null +++ b/test/hw/test_ad9081_smoke.py @@ -0,0 +1,32 @@ +"""Smoke test: open an AD9081 (mini2 = ZCU102 + AD9081) over IIO and read its context. + +Run under the GH Actions HW workflow (places mini2 by manifest) or locally: + pytest -v test/hw/test_ad9081_smoke.py --iio-uri-override ip:10.0.0.23 +""" + +from __future__ import annotations + +import adi + + +def test_context_attrs(iio_uri): + """The AD9081 driver enumerates context attrs (hw_model, etc.).""" + sdr = adi.ad9081(uri=iio_uri) + try: + assert sdr.ctx is not None + assert sdr.ctx.attrs, f"empty context attrs at {iio_uri}" + finally: + del sdr + + +def test_rx_buffer(iio_uri): + """Capture one small RX buffer end-to-end.""" + sdr = adi.ad9081(uri=iio_uri) + try: + sdr.rx_buffer_size = 1024 + data = sdr.rx() + assert data is not None + # ad9081 RX is multi-channel: list of np arrays. At minimum non-empty. + assert len(data) > 0 + finally: + del sdr diff --git a/test/hw/test_ad9361_smoke.py b/test/hw/test_ad9361_smoke.py deleted file mode 100644 index ee343702e9..0000000000 --- a/test/hw/test_ad9361_smoke.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Smoke test: open a libiio context against an AD9361-based board (FMComms2/3). - -Run under the new GH Actions HW workflow with LG_COORDINATOR + LG_PLACE -set (see conftest.py), or locally via: - - pytest -v test/hw/test_ad9361_smoke.py --iio-uri-override ip:10.0.0.50 -""" - -from __future__ import annotations - -import pytest - -import adi - - -def test_open_context(iio_uri): - """ad9361 driver attaches to the iio context and exposes attrs.""" - sdr = adi.ad9361(uri=iio_uri) - try: - assert sdr.ctx is not None - finally: - del sdr - - -def test_rx_buffer(iio_uri): - """Capture a single RX buffer end-to-end.""" - sdr = adi.ad9361(uri=iio_uri) - try: - sdr.rx_buffer_size = 1024 - data = sdr.rx() - assert data is not None - assert len(data) > 0 - finally: - del sdr diff --git a/test/hw/test_ad9371_smoke.py b/test/hw/test_ad9371_smoke.py new file mode 100644 index 0000000000..19ff7a42a2 --- /dev/null +++ b/test/hw/test_ad9371_smoke.py @@ -0,0 +1,31 @@ +"""Smoke test: open an AD9371 (bq = ZC706 + ADRV9371) over IIO. + +Run under the GH Actions HW workflow (places bq by manifest) or locally: + pytest -v test/hw/test_ad9371_smoke.py --iio-uri-override ip: +""" + +from __future__ import annotations + +import adi + + +def test_context_attrs(iio_uri): + """The AD9371 driver enumerates context attrs.""" + sdr = adi.ad9371(uri=iio_uri) + try: + assert sdr.ctx is not None + assert sdr.ctx.attrs, f"empty context attrs at {iio_uri}" + finally: + del sdr + + +def test_rx_buffer(iio_uri): + """Capture one small RX buffer end-to-end.""" + sdr = adi.ad9371(uri=iio_uri) + try: + sdr.rx_buffer_size = 1024 + data = sdr.rx() + assert data is not None + assert len(data) > 0 + finally: + del sdr diff --git a/test/hw/test_adrv9009_smoke.py b/test/hw/test_adrv9009_smoke.py new file mode 100644 index 0000000000..8c787e68b3 --- /dev/null +++ b/test/hw/test_adrv9009_smoke.py @@ -0,0 +1,31 @@ +"""Smoke test: open an ADRV9009 (nemo = ZC706 + ADRV9009) over IIO. + +Run under the GH Actions HW workflow (places nemo by manifest) or locally: + pytest -v test/hw/test_adrv9009_smoke.py --iio-uri-override ip: +""" + +from __future__ import annotations + +import adi + + +def test_context_attrs(iio_uri): + """The ADRV9009 driver enumerates context attrs.""" + sdr = adi.adrv9009(uri=iio_uri) + try: + assert sdr.ctx is not None + assert sdr.ctx.attrs, f"empty context attrs at {iio_uri}" + finally: + del sdr + + +def test_rx_buffer(iio_uri): + """Capture one small RX buffer end-to-end.""" + sdr = adi.adrv9009(uri=iio_uri) + try: + sdr.rx_buffer_size = 1024 + data = sdr.rx() + assert data is not None + assert len(data) > 0 + finally: + del sdr diff --git a/test/hw/test_pluto_smoke.py b/test/hw/test_pluto_smoke.py deleted file mode 100644 index a327c9d880..0000000000 --- a/test/hw/test_pluto_smoke.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Smoke test: open a libiio context against a PlutoSDR and read its product ID. - -Run under the new GH Actions HW workflow with LG_COORDINATOR + LG_PLACE -set (see conftest.py), or locally via: - - pytest -v test/hw/test_pluto_smoke.py --iio-uri-override ip:192.168.2.1 -""" - -from __future__ import annotations - -import pytest - -import adi - - -def test_open_context(iio_uri): - """Pluto answers ctx.attrs and exposes its product ID.""" - sdr = adi.Pluto(uri=iio_uri) - try: - # `product_id` lives in libiio context attrs; raise if it's missing. - ctx_attrs = dict(sdr.ctx.attrs) - assert ctx_attrs, f"context attrs empty for {iio_uri}" - finally: - del sdr - - -def test_rx_buffer(iio_uri): - """Capture a single RX buffer end-to-end.""" - sdr = adi.Pluto(uri=iio_uri) - try: - sdr.rx_buffer_size = 1024 - data = sdr.rx() - assert data is not None - assert len(data) == sdr.rx_buffer_size - finally: - del sdr From 135faaf8216d04476f2cc604079d7990dd5a3fc1 Mon Sep 17 00:00:00 2001 From: Travis Collins Date: Thu, 14 May 2026 02:18:24 +0000 Subject: [PATCH 04/24] fix(hw): use venv labgrid-client binary, fail instead of skip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes: * Replace 'python -m labgrid.remote.client' with the venv's labgrid-client script. The module-style invocation against the pinned tfcollins/labgrid SHA swallows stdout in some paths, which made every test silently skip with "Place" not found. * Drop the silent skip — pytest.fail instead, so missing places or missing addresses are loud red. The reusable workflow's acquire-place composite has already acquired the place; if show can't find it now, something is genuinely wrong. Also always echo the raw 'labgrid-client show' stdout/stderr to test-stderr so a CI re-run doesn't require another debug roundtrip. --- test/hw/conftest.py | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/test/hw/conftest.py b/test/hw/conftest.py index 7ad7764e25..ddf912b330 100644 --- a/test/hw/conftest.py +++ b/test/hw/conftest.py @@ -36,18 +36,19 @@ def pytest_addoption(parser): def _labgrid_show(coord: str, place: str): - """Run `python -m labgrid.remote.client -p PLACE show` and return stdout.""" + """Run `labgrid-client -p PLACE show` and return the CompletedProcess. + + Prefer the venv's installed `labgrid-client` binary over + `python -m labgrid.remote.client` — the latter routes async output + through a different code path that occasionally swallows stdout in + the fork we pin to. + """ + venv_bin = os.path.dirname(sys.executable) + lgclient = os.path.join(venv_bin, "labgrid-client") + if not os.path.isfile(lgclient): + lgclient = "labgrid-client" # fall back to PATH return subprocess.run( - [ - sys.executable, - "-m", - "labgrid.remote.client", - "-x", - coord, - "-p", - place, - "show", - ], + [lgclient, "-x", coord, "-p", place, "show"], capture_output=True, text=True, check=False, @@ -83,10 +84,18 @@ def iio_uri(request) -> str: pytest.skip("labgrid not importable; pass --iio-uri-override instead") show = _labgrid_show(coord, place) + # Always print the raw show output so debugging a CI skip doesn't + # require yet another roundtrip. + print( + f"[iio_uri] labgrid-client show {place} on {coord}:\n" + f"--- stdout ---\n{show.stdout}\n--- stderr ---\n{show.stderr}\n" + f"--- rc={show.returncode}", + file=sys.stderr, + ) if show.returncode != 0 or "Place" not in show.stdout: - pytest.skip( - f"labgrid place {place!r} unavailable at {coord}: " - f"{show.stdout.strip()} {show.stderr.strip()}" + pytest.fail( + f"labgrid place {place!r} unavailable at {coord} " + f"(rc={show.returncode}); see stderr for raw output" ) address = None @@ -96,7 +105,7 @@ def iio_uri(request) -> str: address = s.split(":", 1)[1].strip() break if not address: - pytest.skip( + pytest.fail( f"labgrid place {place!r} has no NetworkService address — " "verify the exporter publishes one" ) From e0d811ddd11bd7c87d760cd6cf89d86253af5247 Mon Sep 17 00:00:00 2001 From: Travis Collins Date: Thu, 14 May 2026 02:22:11 +0000 Subject: [PATCH 05/24] fix(hw/conftest): parse Python-repr 'address' format from labgrid show The tfcollins/labgrid fork prints NetworkService params as Python repr ('address': '10.0.0.23'), not flat key-value (address: 10.0.0.23). The line-prefix matcher missed both; regex now captures either form. --- test/hw/conftest.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/test/hw/conftest.py b/test/hw/conftest.py index ddf912b330..5c9d524087 100644 --- a/test/hw/conftest.py +++ b/test/hw/conftest.py @@ -20,6 +20,7 @@ from __future__ import annotations import os +import re import subprocess import sys @@ -98,12 +99,15 @@ def iio_uri(request) -> str: f"(rc={show.returncode}); see stderr for raw output" ) + # Handle both the flat 'address: 10.0.0.23' format and the Python-repr + # 'address': '10.0.0.23' style emitted by the labgrid fork. address = None - for line in show.stdout.splitlines(): - s = line.strip() - if s.startswith(("address:", "host:", "ipaddr:")): - address = s.split(":", 1)[1].strip() - break + m = re.search( + r"""['"]?(?:address|host|ipaddr)['"]?\s*[:=]\s*['"]?([0-9a-zA-Z.\-]+)""", + show.stdout, + ) + if m: + address = m.group(1) if not address: pytest.fail( f"labgrid place {place!r} has no NetworkService address — " From d2eabbf01186c449ad81a8fdf8d5477caf1c8401 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 14 May 2026 09:53:30 -0400 Subject: [PATCH 06/24] feat(hw-ci): migrate to hw-matrix.yml@v2 (discovery-driven) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacks on PR #747 (feature/gha-hw-workflow). Once that merges, this PR rebases onto main and lands the v2 migration. Discovery-driven flow lives entirely in labgrid-plugins (PR #16). On the consumer side this PR is a deletion-heavy diff: * Delete .github/hw-nodes.json (the v1 static per-place manifest). * Delete test/hw/env/{mini2,bq,nemo}.yaml (v1 per-place labgrid env files). The reusable workflow renders the env yaml on the fly from the place's `boot-strategy` tag. * Drop the !test/hw/env/ exception line in .gitignore. * Update .github/workflows/hardware-test.yml: switch the `uses:` ref to hw-matrix-v2.yml; drop manifest_path + legs inputs; add marker_filter=iio_hardware + test_root=test/hw; install adi-labgrid-plugins in the venv (it provides both the marker plugin and the adi-lg-hw-ci CLI the workflow shells out to). * Add @pytest.mark.iio_hardware(["..."]) decorators to the three smoke tests so the v2 matrix discovers them. Carrier-narrowing not used yet — these tests don't depend on a specific carrier. * Rewrite test/hw/README.md to describe the v2 model (no manifest, no env yaml, marker-driven, lab-admin sets tags). Workflow ref pinned to labgrid-plugins@feature/hw-matrix-v2-discovery until that PR merges and the v2 tag is cut. Flip to @v2 in a follow-up commit before this PR merges. Test plan: * actionlint .github/workflows/hardware-test.yml → clean. * pytest --collect-only -m iio_hardware against test/hw/ in a venv with adi-labgrid-plugins installed → 6 tests collected; the --hw-ci-export-markers JSON correctly shapes each entry as {iio_hardware: [...], iio_carrier: []}. * Once the lab admin tags mini2 / bq / nemo on the live 10.0.0.41:20408 coordinator, dispatching this workflow should produce one matrix shard per (place, daughter-board) pair and skip-with-annotation any board that isn't live. Migration status: * v1 (hw-matrix.yml@v1) stays in labgrid-plugins indefinitely for any consumer that hasn't migrated yet. * The legacy JenkinsfileHW pipeline is untouched. --- .github/hw-nodes.json | 23 --------- .github/workflows/hardware-test.yml | 31 ++++++++---- .gitignore | 1 - test/hw/README.md | 76 +++++++++++++++++++---------- test/hw/env/bq.yaml | 6 --- test/hw/env/mini2.yaml | 8 --- test/hw/env/nemo.yaml | 6 --- test/hw/test_ad9081_smoke.py | 10 +++- test/hw/test_ad9371_smoke.py | 11 ++++- test/hw/test_adrv9009_smoke.py | 10 +++- 10 files changed, 96 insertions(+), 86 deletions(-) delete mode 100644 .github/hw-nodes.json delete mode 100644 test/hw/env/bq.yaml delete mode 100644 test/hw/env/mini2.yaml delete mode 100644 test/hw/env/nemo.yaml diff --git a/.github/hw-nodes.json b/.github/hw-nodes.json deleted file mode 100644 index 57616c6855..0000000000 --- a/.github/hw-nodes.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - { - "place": "mini2", - "runner_label": "hw-mini2", - "env_remote": "test/hw/env/mini2.yaml", - "tests": ["test/hw/test_ad9081_smoke.py"], - "legs": ["coord"] - }, - { - "place": "bq", - "runner_label": "hw-bq", - "env_remote": "test/hw/env/bq.yaml", - "tests": ["test/hw/test_ad9371_smoke.py"], - "legs": ["coord"] - }, - { - "place": "nemo", - "runner_label": "hw-nemo", - "env_remote": "test/hw/env/nemo.yaml", - "tests": ["test/hw/test_adrv9009_smoke.py"], - "legs": ["coord"] - } -] diff --git a/.github/workflows/hardware-test.yml b/.github/workflows/hardware-test.yml index 50be3349b2..4827bf2996 100644 --- a/.github/workflows/hardware-test.yml +++ b/.github/workflows/hardware-test.yml @@ -1,15 +1,17 @@ name: Hardware Tests (GHA) -# Net-new GHA HW pipeline running alongside JenkinsfileHW during migration. -# Calls the shared HW-CI workflow in tfcollins/labgrid-plugins. +# Discovery-driven HW pipeline (v2). Lab coordinator advertises live +# places via `tags:` (carrier, daughter-board, boot-strategy); this +# repo's tests carry `@pytest.mark.iio_hardware([…])` markers; the +# reusable workflow intersects the two and builds the matrix. +# +# No `.github/hw-nodes.json`, no `test/hw/env/*.yaml`. The env yaml is +# rendered per-shard from the place tags by adi-labgrid-plugins. # # Triggered by: # * workflow_dispatch (manual) # * PRs labeled `hw-test` — keeps every push from burning lab time # * Nightly cron at 08:00 UTC -# -# Initial scope: `pluto` and `fmcomms2-zcu102` places (coord leg only). -# Add boards by extending .github/hw-nodes.json and test/hw/. permissions: contents: read @@ -28,20 +30,31 @@ jobs: if: >- github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'hw-test') - uses: tfcollins/labgrid-plugins/.github/workflows/hw-matrix.yml@v1 + # TODO: change ref to @v2 after tfcollins/labgrid-plugins#16 merges + # and the v2 tag is cut. Pinned to the feature branch for now so + # the discovery-driven flow can be exercised end-to-end before tag. + uses: tfcollins/labgrid-plugins/.github/workflows/hw-matrix-v2.yml@feature/hw-matrix-v2-discovery with: - manifest_path: .github/hw-nodes.json + marker_filter: iio_hardware + test_root: test/hw venv_install_cmd: | uv pip install --quiet --python "$VENV_DIR/bin/python" \ -r requirements_dev.txt uv pip install --quiet --python "$VENV_DIR/bin/python" -e . uv pip install --quiet --python "$VENV_DIR/bin/python" \ "labgrid @ git+https://github.com/tfcollins/labgrid.git@81c35eda6436af63d3acf7f8b98893abae0bbbca" + # adi-labgrid-plugins supplies BOTH the iio_hardware/iio_carrier + # marker plugin (auto-registered via pytest11 entry point) AND + # the adi-lg-hw-ci CLI the reusable workflow shells out to. + uv pip install --quiet --python "$VENV_DIR/bin/python" \ + "adi-labgrid-plugins @ git+https://github.com/tfcollins/labgrid-plugins.git@feature/hw-matrix-v2-discovery" pytest_cmd_template: >- - "$VENV_DIR/bin/pytest" -v $TESTS --junitxml="$JUNIT" + "$VENV_DIR/bin/pytest" -v test/hw + -m "$MARKER_FILTER" + --lg-config "$LG_ENV" + --junitxml="$JUNIT" artifact_glob: | junit-hw-*.xml **/*.log - legs: coord prism_project: pyadi-iio secrets: inherit diff --git a/.gitignore b/.gitignore index 6bf5e0c356..dee43f70bb 100644 --- a/.gitignore +++ b/.gitignore @@ -85,7 +85,6 @@ celerybeat-schedule .env .venv env/ -!test/hw/env/ venv/ ENV/ env.bak/ diff --git a/test/hw/README.md b/test/hw/README.md index 5e29e84aed..4c41dc3038 100644 --- a/test/hw/README.md +++ b/test/hw/README.md @@ -2,37 +2,53 @@ Minimal pyadi-iio tests that run against real boards held by a labgrid coordinator. Wired into CI by `.github/workflows/hardware-test.yml`, -which calls the shared `hw-matrix` reusable workflow in -`tfcollins/labgrid-plugins`. +which calls the shared **discovery-driven** `hw-matrix-v2.yml` reusable +workflow in `tfcollins/labgrid-plugins`. This suite is **independent** of the historic `test/test_*.py` files under `test/` (which use the `pytest-libiio` plugin and a host_map). It exists so the new GH Actions HW pipeline has something to run while the existing suite stays on Jenkins during the migration. +## How the matrix is built (v2 model) + +The reusable workflow doesn't read any per-repo manifest. Instead, on +every run it: + +1. Queries the coordinator for live places (each tagged with `carrier`, + `daughter-board`, `boot-strategy`). +2. Runs `pytest --collect-only -m iio_hardware` against this repo and + harvests every test's `@pytest.mark.iio_hardware([...])` and optional + `@pytest.mark.iio_carrier([...])` markers. +3. Builds one matrix shard per (place, daughter-board) intersection — + per-shard pytest runs `pytest -m "iio_hardware and "`. + +If a place isn't live, its shard doesn't appear. If we mark a test for +hardware that no live place advertises, the workflow emits a clear +"no overlap right now" annotation instead of failing. + ## Layout ``` test/hw/ -├── conftest.py # iio_uri fixture: labgrid place → ip:URI -├── env/ -│ ├── pluto.yaml # coord-mode env for the 'pluto' place -│ └── fmcomms2_zcu102.yaml -├── test_pluto_smoke.py # smoke for adi.Pluto -└── test_ad9361_smoke.py # smoke for adi.ad9361 (FMComms2/3 carrier) +├── conftest.py # iio_uri fixture: labgrid place → ip:URI +├── test_ad9081_smoke.py # @pytest.mark.iio_hardware(["ad9081"]) +├── test_ad9371_smoke.py # @pytest.mark.iio_hardware(["adrv9371", "ad9371"]) +└── test_adrv9009_smoke.py # @pytest.mark.iio_hardware(["adrv9009"]) ``` +No per-place env yamls. The reusable workflow renders the labgrid env +yaml on the fly from each place's `boot-strategy` tag. + ## How a test resolves a URI `conftest.py::iio_uri` (session-scoped) returns an `ip:...` URI by: -1. **`--iio-uri-override `** or **`IIO_URI_OVERRIDE` env var** — bypass - labgrid entirely (for laptop runs against a known-good DUT). -2. **`LG_COORDINATOR` + `LG_PLACE`** — set by the HW workflow. The - workflow's acquire-place composite has already acquired the place - before pytest starts; conftest runs `labgrid-client show` and parses - the first `address:` / `host:` / `ipaddr:` line under the held - `NetworkService` resource. +1. **`--iio-uri-override `** or **`IIO_URI_OVERRIDE` env var** — + bypass labgrid entirely (for laptop runs against a known-good DUT). +2. **`LG_COORDINATOR` + `LG_PLACE`** — set by the HW workflow before + pytest starts. Conftest queries the coordinator and extracts the + place's `NetworkService.address`. If neither source is set, every test under `test/hw/` is skipped. @@ -40,23 +56,29 @@ If neither source is set, every test under `test/hw/` is skipped. ```bash # Without labgrid (point at an iiod yourself) -pytest -v test/hw/test_pluto_smoke.py --iio-uri-override ip:192.168.2.1 +pytest -v test/hw/test_ad9081_smoke.py --iio-uri-override ip:10.0.0.23 -# Against the lab coordinator -LG_COORDINATOR=10.0.0.41:20408 LG_PLACE=pluto \ - pytest -v test/hw/test_pluto_smoke.py +# Against the lab coordinator (acquires + shows the place) +LG_COORDINATOR=10.0.0.41:20408 LG_PLACE=mini2 \ + pytest -v test/hw/test_ad9081_smoke.py ``` -## Adding a new board +## Adding a new board family -1. Add an entry to `.github/hw-nodes.json` (place + runner_label + the - test files to run for that place; `legs: coord` for now). -2. Create `test/hw/env/.yaml` mirroring the structure of the - existing files — only the `RemotePlace.name` needs to change. -3. Write `test/hw/test__smoke.py`. Take the `iio_uri` fixture, +1. Write `test/hw/test__smoke.py`. Take the `iio_uri` fixture, instantiate the right `adi.(uri=iio_uri)`, exercise one or - two attributes plus a buffer. Don't reuse the heavyweight fixtures - from `test/conftest.py`. + two attributes plus a buffer. +2. Decorate each test with `@pytest.mark.iio_hardware([])`. Use + the same name the coordinator's `daughter-board` tag uses (or a list + that includes aliases). +3. (Optional) Add `@pytest.mark.iio_carrier(["zcu102"])` if the test + only works on a specific carrier. +4. (Lab admin) Ensure the live coordinator has at least one place + tagged `daughter-board=` and `boot-strategy=`. + +No workflow edit needed. The discover step picks the new tests up on +the next run. ## Running alongside Jenkins diff --git a/test/hw/env/bq.yaml b/test/hw/env/bq.yaml deleted file mode 100644 index d93729f8ca..0000000000 --- a/test/hw/env/bq.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Coord-mode env for bq (ZC706 + ADRV9371). See mini2.yaml for the pattern. -targets: - main: - resources: - RemotePlace: - name: bq diff --git a/test/hw/env/mini2.yaml b/test/hw/env/mini2.yaml deleted file mode 100644 index 7d4206b3c7..0000000000 --- a/test/hw/env/mini2.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Coord-mode env for mini2 (ZCU102 + AD9081). Minimal: just RemotePlace so -# labgrid resolves the place; the conftest reads LG_PLACE / LG_COORDINATOR -# directly to extract the IIO URI without invoking labgrid drivers. -targets: - main: - resources: - RemotePlace: - name: mini2 diff --git a/test/hw/env/nemo.yaml b/test/hw/env/nemo.yaml deleted file mode 100644 index e1809efb74..0000000000 --- a/test/hw/env/nemo.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Coord-mode env for nemo (ZC706 + ADRV9009). See mini2.yaml for the pattern. -targets: - main: - resources: - RemotePlace: - name: nemo diff --git a/test/hw/test_ad9081_smoke.py b/test/hw/test_ad9081_smoke.py index 4e86ea7c4c..0283dd637c 100644 --- a/test/hw/test_ad9081_smoke.py +++ b/test/hw/test_ad9081_smoke.py @@ -1,14 +1,19 @@ -"""Smoke test: open an AD9081 (mini2 = ZCU102 + AD9081) over IIO and read its context. +"""Smoke test: open an AD9081 over IIO and read its context. -Run under the GH Actions HW workflow (places mini2 by manifest) or locally: +Discovered by labgrid-plugins hw-matrix.yml@v2 — any coordinator place +tagged ``daughter-board=ad9081`` will fan a shard of this file out. + +Local manual run: pytest -v test/hw/test_ad9081_smoke.py --iio-uri-override ip:10.0.0.23 """ from __future__ import annotations import adi +import pytest +@pytest.mark.iio_hardware(["ad9081"]) def test_context_attrs(iio_uri): """The AD9081 driver enumerates context attrs (hw_model, etc.).""" sdr = adi.ad9081(uri=iio_uri) @@ -19,6 +24,7 @@ def test_context_attrs(iio_uri): del sdr +@pytest.mark.iio_hardware(["ad9081"]) def test_rx_buffer(iio_uri): """Capture one small RX buffer end-to-end.""" sdr = adi.ad9081(uri=iio_uri) diff --git a/test/hw/test_ad9371_smoke.py b/test/hw/test_ad9371_smoke.py index 19ff7a42a2..fa3a15e003 100644 --- a/test/hw/test_ad9371_smoke.py +++ b/test/hw/test_ad9371_smoke.py @@ -1,14 +1,20 @@ -"""Smoke test: open an AD9371 (bq = ZC706 + ADRV9371) over IIO. +"""Smoke test: open an AD9371 / ADRV9371 over IIO. -Run under the GH Actions HW workflow (places bq by manifest) or locally: +Discovered by labgrid-plugins hw-matrix.yml@v2 — any coordinator place +tagged ``daughter-board=adrv9371`` (or the legacy ``ad9371`` alias) +will fan a shard of this file out. + +Local manual run: pytest -v test/hw/test_ad9371_smoke.py --iio-uri-override ip: """ from __future__ import annotations import adi +import pytest +@pytest.mark.iio_hardware(["adrv9371", "ad9371"]) def test_context_attrs(iio_uri): """The AD9371 driver enumerates context attrs.""" sdr = adi.ad9371(uri=iio_uri) @@ -19,6 +25,7 @@ def test_context_attrs(iio_uri): del sdr +@pytest.mark.iio_hardware(["adrv9371", "ad9371"]) def test_rx_buffer(iio_uri): """Capture one small RX buffer end-to-end.""" sdr = adi.ad9371(uri=iio_uri) diff --git a/test/hw/test_adrv9009_smoke.py b/test/hw/test_adrv9009_smoke.py index 8c787e68b3..f32ea1505b 100644 --- a/test/hw/test_adrv9009_smoke.py +++ b/test/hw/test_adrv9009_smoke.py @@ -1,14 +1,19 @@ -"""Smoke test: open an ADRV9009 (nemo = ZC706 + ADRV9009) over IIO. +"""Smoke test: open an ADRV9009 over IIO. -Run under the GH Actions HW workflow (places nemo by manifest) or locally: +Discovered by labgrid-plugins hw-matrix.yml@v2 — any coordinator place +tagged ``daughter-board=adrv9009`` will fan a shard of this file out. + +Local manual run: pytest -v test/hw/test_adrv9009_smoke.py --iio-uri-override ip: """ from __future__ import annotations import adi +import pytest +@pytest.mark.iio_hardware(["adrv9009"]) def test_context_attrs(iio_uri): """The ADRV9009 driver enumerates context attrs.""" sdr = adi.adrv9009(uri=iio_uri) @@ -19,6 +24,7 @@ def test_context_attrs(iio_uri): del sdr +@pytest.mark.iio_hardware(["adrv9009"]) def test_rx_buffer(iio_uri): """Capture one small RX buffer end-to-end.""" sdr = adi.adrv9009(uri=iio_uri) From ea33ead0eae4293bccb3ebf99b9217f58e37de7f Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 14 May 2026 10:43:46 -0400 Subject: [PATCH 07/24] ci: retrigger hardware-test on labgrid-plugins runner-label fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No code change. The labgrid-plugins reusable workflow at @feature/hw-matrix-v2-discovery has been updated (PR #16) so the `discover` job pins to `hw-coordinator` and each `hw` shard pins to `hw-` — matching the lab's real self-hosted runner labels. The prior run (25865035443) sat in `queued` indefinitely because the old default `hw-coord` matched no runner. This commit re-fires the workflow against the fixed reusable workflow. From 904b87d23cafcf877d705e864d61c343fff1c0d7 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 14 May 2026 10:47:04 -0400 Subject: [PATCH 08/24] =?UTF-8?q?ci:=20retrigger=20after=20labgrid-plugins?= =?UTF-8?q?=20REST=E2=86=92CLI=20fallback=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 8594575de9f72565e1e463450d008cfa43c9861f Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 14 May 2026 10:49:40 -0400 Subject: [PATCH 09/24] ci: retrigger after marker-harvest plugin-autoload fix From 5f4d8e09503d07b663891fe907b6175707065f33 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 14 May 2026 10:56:40 -0400 Subject: [PATCH 10/24] ci: retrigger after AST marker harvest From a142f063d2a5d17dadd612e7971e7854eddf9af2 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 14 May 2026 11:05:37 -0400 Subject: [PATCH 11/24] ci: retrigger after boot-strategy tag added to mini2/bq/nemo From 9ae4fd2c8dc49430fbd08177dc3da80ef2a266e6 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 14 May 2026 11:09:20 -0400 Subject: [PATCH 12/24] fix(hw-ci): pytest uses --lg-env, not --lg-config labgrid's pytest plugin registers --lg-env. The v2 caller had a typo that pytest rejected with 'unrecognized arguments: --lg-config' before any test ran. --- .github/workflows/hardware-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hardware-test.yml b/.github/workflows/hardware-test.yml index 4827bf2996..2bf6960088 100644 --- a/.github/workflows/hardware-test.yml +++ b/.github/workflows/hardware-test.yml @@ -51,7 +51,7 @@ jobs: pytest_cmd_template: >- "$VENV_DIR/bin/pytest" -v test/hw -m "$MARKER_FILTER" - --lg-config "$LG_ENV" + --lg-env "$LG_ENV" --junitxml="$JUNIT" artifact_glob: | junit-hw-*.xml From fc4fbea6c573ebef0b0069e425975a82e5ce8dcd Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 14 May 2026 11:16:30 -0400 Subject: [PATCH 13/24] ci: retrigger after HW_DAUGHTER/HW_CARRIER narrowing fix From 128d641e2f1f630fceed403939a40af5f3a814fd Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 14 May 2026 11:25:10 -0400 Subject: [PATCH 14/24] ci: retrigger after v2 acquire+boot+release added From c809f08521f3f8611b30652046a0e84aac8d0961 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 14 May 2026 11:32:31 -0400 Subject: [PATCH 15/24] fix(hw-ci): install adi-labgrid-plugins[kuiper] for pytsk3 BootFPGASoC.__attrs_post_init__ unconditionally calls KuiperDLDriver.get_boot_files_from_release() which imports pytsk3. pytsk3 is declared as an optional extra in adi-labgrid-plugins (see pyproject.toml [kuiper]); without it, every shard's boot step crashes with ModuleNotFoundError before pytest runs. Tack [kuiper] on the install line so uv pulls pytsk3 in. --- .github/workflows/hardware-test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/hardware-test.yml b/.github/workflows/hardware-test.yml index 2bf6960088..0b3d3d4525 100644 --- a/.github/workflows/hardware-test.yml +++ b/.github/workflows/hardware-test.yml @@ -46,8 +46,10 @@ jobs: # adi-labgrid-plugins supplies BOTH the iio_hardware/iio_carrier # marker plugin (auto-registered via pytest11 entry point) AND # the adi-lg-hw-ci CLI the reusable workflow shells out to. + # `[kuiper]` pulls in pytsk3, required by KuiperDLDriver + # for extracting boot files at strategy init time. uv pip install --quiet --python "$VENV_DIR/bin/python" \ - "adi-labgrid-plugins @ git+https://github.com/tfcollins/labgrid-plugins.git@feature/hw-matrix-v2-discovery" + "adi-labgrid-plugins[kuiper] @ git+https://github.com/tfcollins/labgrid-plugins.git@feature/hw-matrix-v2-discovery" pytest_cmd_template: >- "$VENV_DIR/bin/pytest" -v test/hw -m "$MARKER_FILTER" From 26a2068515364598348efaf63cce24a0f6aad0c4 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 14 May 2026 11:42:01 -0400 Subject: [PATCH 16/24] ci: retrigger after power-driver tag + wait-for-iiod From c1f85bb18b4875116ae0b4cc757a6209dde258ef Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 14 May 2026 11:52:30 -0400 Subject: [PATCH 17/24] ci: retrigger after HomeAssistantPowerDriver class-name fix From 465988e816de74b49af4fc5f155cf1c7628ad572 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 14 May 2026 11:58:39 -0400 Subject: [PATCH 18/24] ci: retrigger after bq/nemo retagged to BootFPGASoCSSH From 9b5fe0916e2533e65fae13b3eee3e964f1a0c4c5 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 14 May 2026 12:05:51 -0400 Subject: [PATCH 19/24] ci: retrigger after BootFPGASoCSSH SSHDriver wiring fix From 03301b864ad00ab8de08ace60cc9e26a2cab9142 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 14 May 2026 12:37:16 -0400 Subject: [PATCH 20/24] ci: retrigger after serial-based IP self-resolve From 768e61290e2e16f49d9af9a2c84ce0ae600369d1 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 14 May 2026 13:01:34 -0400 Subject: [PATCH 21/24] fix(hw-ci): pass DUT URI explicitly to pytest; drop conftest auto-discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the conftest fell back to a labgrid-show lookup whenever --iio-uri-override wasn't passed. That meant the test could end up poking whichever board the coordinator advertised — and we just learned the hard way (mini2 ran into this) that NetworkService.address goes stale across DHCP renewals. Make the URI an explicit pytest CLI argument: * hardware-test.yml's pytest_cmd_template adds `--iio-uri-override "$IIO_URI_OVERRIDE"`. The hw-matrix-v2 workflow resolves the IP from the live board's serial console and exports IIO_URI_OVERRIDE before pytest runs. * test/hw/conftest.py drops the labgrid auto-discovery path entirely. If no URI was provided, the fixture skips with a clear message instead of silently connecting to whatever the coordinator says. This eliminates the "what if I'm testing a random board on the LAN?" class of failure. The workflow IS the contract. --- .github/workflows/hardware-test.yml | 1 + test/hw/README.md | 20 +++-- test/hw/conftest.py | 123 +++++++--------------------- 3 files changed, 43 insertions(+), 101 deletions(-) diff --git a/.github/workflows/hardware-test.yml b/.github/workflows/hardware-test.yml index 0b3d3d4525..17323246f6 100644 --- a/.github/workflows/hardware-test.yml +++ b/.github/workflows/hardware-test.yml @@ -54,6 +54,7 @@ jobs: "$VENV_DIR/bin/pytest" -v test/hw -m "$MARKER_FILTER" --lg-env "$LG_ENV" + --iio-uri-override "$IIO_URI_OVERRIDE" --junitxml="$JUNIT" artifact_glob: | junit-hw-*.xml diff --git a/test/hw/README.md b/test/hw/README.md index 4c41dc3038..03811b95bf 100644 --- a/test/hw/README.md +++ b/test/hw/README.md @@ -42,15 +42,17 @@ yaml on the fly from each place's `boot-strategy` tag. ## How a test resolves a URI -`conftest.py::iio_uri` (session-scoped) returns an `ip:...` URI by: - -1. **`--iio-uri-override `** or **`IIO_URI_OVERRIDE` env var** — - bypass labgrid entirely (for laptop runs against a known-good DUT). -2. **`LG_COORDINATOR` + `LG_PLACE`** — set by the HW workflow before - pytest starts. Conftest queries the coordinator and extracts the - place's `NetworkService.address`. - -If neither source is set, every test under `test/hw/` is skipped. +`conftest.py::iio_uri` (session-scoped) requires the URI to be passed +**explicitly** via `--iio-uri-override ` (the `IIO_URI_OVERRIDE` +env var is honoured as a default). The conftest deliberately refuses +to discover a URI from labgrid or the coordinator — making the URI +explicit means CI can never accidentally talk to a stray board that +happens to be on the lab LAN. + +The CI workflow handles URI resolution itself: it acquires the place, +boots the board, captures the live serial console, parses eth0's +DHCP-assigned address, and passes `--iio-uri-override ip:` +to pytest. Tests skip cleanly when no URI is provided. ## Running locally diff --git a/test/hw/conftest.py b/test/hw/conftest.py index 5c9d524087..5cb6bb569e 100644 --- a/test/hw/conftest.py +++ b/test/hw/conftest.py @@ -1,28 +1,24 @@ """HW-test conftest. -Bridges the labgrid place held by the calling workflow (or a manually-set -LG_PLACE / LG_COORDINATOR pair) into a libiio URI usable by ``adi`` -device classes. - -Used by the new GH Actions hardware-test workflow (which calls -labgrid-plugins' hw-matrix reusable workflow). Tests under ``test/hw/`` -take an ``iio_uri`` fixture and instantiate the device class with it. - -When LG_ENV / LG_PLACE / LG_COORDINATOR are unset, the suite falls back -to a single ``--iio-uri-override`` CLI option (or the IIO_URI_OVERRIDE -env var) so local manual runs without labgrid still work. - -Sibling test/conftest.py (the historic libiio-plugin-driven suite under -test/) is untouched; this conftest only applies to tests collected -under test/hw/. +Resolves a libiio URI for tests under ``test/hw/``. The URI is **always** +supplied explicitly via ``--iio-uri-override``; there is no implicit +labgrid/coordinator lookup. The caller (CI workflow or local user) is +responsible for naming the exact board to talk to — this avoids any +chance of pytest connecting to a stray board that happens to be on the +network. + +Used by the GH Actions hardware-test workflow, which acquires the +place, boots it, reads eth0's IP from the live serial console, and +passes ``--iio-uri-override ip:`` to pytest. + +Sibling ``test/conftest.py`` (the historic libiio-plugin-driven suite +under ``test/``) is untouched; this conftest only applies to tests +collected under ``test/hw/``. """ from __future__ import annotations import os -import re -import subprocess -import sys import pytest @@ -31,86 +27,29 @@ def pytest_addoption(parser): g = parser.getgroup("hw") g.addoption( "--iio-uri-override", - default=os.environ.get("IIO_URI_OVERRIDE"), - help="Bypass labgrid; libiio URI to point tests at (e.g. ip:10.0.0.132).", - ) - - -def _labgrid_show(coord: str, place: str): - """Run `labgrid-client -p PLACE show` and return the CompletedProcess. - - Prefer the venv's installed `labgrid-client` binary over - `python -m labgrid.remote.client` — the latter routes async output - through a different code path that occasionally swallows stdout in - the fork we pin to. - """ - venv_bin = os.path.dirname(sys.executable) - lgclient = os.path.join(venv_bin, "labgrid-client") - if not os.path.isfile(lgclient): - lgclient = "labgrid-client" # fall back to PATH - return subprocess.run( - [lgclient, "-x", coord, "-p", place, "show"], - capture_output=True, - text=True, - check=False, - timeout=15, + default=os.environ.get("IIO_URI_OVERRIDE", ""), + help=( + "libiio URI for the DUT (e.g. ip:10.0.0.211). Required for tests " + "under test/hw/. Defaults to the IIO_URI_OVERRIDE env var when " + "unset on the CLI." + ), ) @pytest.fixture(scope="session") def iio_uri(request) -> str: - """Return an `ip:...` URI to a real-iiod we can talk to. + """Return the explicitly-named DUT URI. - Resolution order: - 1. ``--iio-uri-override `` or ``IIO_URI_OVERRIDE`` env var - 2. The labgrid place named by ``LG_PLACE`` on ``LG_COORDINATOR`` - (set by the reusable HW workflow before pytest runs; the - place is already acquired at workflow level). + Tests are skipped if no URI was passed — the conftest deliberately + refuses to auto-discover, so an unrelated board on the LAN can't be + poked by accident. """ - override = request.config.getoption("--iio-uri-override") - if override: - return override - - coord = os.environ.get("LG_COORDINATOR") - place = os.environ.get("LG_PLACE") - if not (coord and place): + uri = (request.config.getoption("--iio-uri-override") or "").strip() + if not uri: pytest.skip( - "no IIO URI source: set --iio-uri-override, or run under the " - "HW workflow which exports LG_COORDINATOR + LG_PLACE" - ) - - try: - import labgrid # noqa: F401 - except ImportError: - pytest.skip("labgrid not importable; pass --iio-uri-override instead") - - show = _labgrid_show(coord, place) - # Always print the raw show output so debugging a CI skip doesn't - # require yet another roundtrip. - print( - f"[iio_uri] labgrid-client show {place} on {coord}:\n" - f"--- stdout ---\n{show.stdout}\n--- stderr ---\n{show.stderr}\n" - f"--- rc={show.returncode}", - file=sys.stderr, - ) - if show.returncode != 0 or "Place" not in show.stdout: - pytest.fail( - f"labgrid place {place!r} unavailable at {coord} " - f"(rc={show.returncode}); see stderr for raw output" - ) - - # Handle both the flat 'address: 10.0.0.23' format and the Python-repr - # 'address': '10.0.0.23' style emitted by the labgrid fork. - address = None - m = re.search( - r"""['"]?(?:address|host|ipaddr)['"]?\s*[:=]\s*['"]?([0-9a-zA-Z.\-]+)""", - show.stdout, - ) - if m: - address = m.group(1) - if not address: - pytest.fail( - f"labgrid place {place!r} has no NetworkService address — " - "verify the exporter publishes one" + "no DUT URI provided. Pass --iio-uri-override ip: on the " + "pytest CLI (the reusable hw-matrix-v2 workflow does this " + "automatically after reading eth0 from the booted board's " + "serial console)." ) - return f"ip:{address}" + return uri From 98946a018afa602a6cf668feb307c44346522d5c Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 14 May 2026 17:06:13 -0400 Subject: [PATCH 22/24] ci: retrigger after nemo ser2net install + UART connected From ed4497e37b241ee7d91b7cb3e079bc3c4074eb73 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 14 May 2026 17:13:46 -0400 Subject: [PATCH 23/24] ci: retrigger after dropping strategy-driven boot for nemo+bq From 15808a662da9dc7ba8ad3ada6870b010384960af Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 14 May 2026 18:45:08 -0400 Subject: [PATCH 24/24] ci: retrigger after microcom installed on bq+nemo runners