diff --git a/.github/workflows/hardware-test.yml b/.github/workflows/hardware-test.yml new file mode 100644 index 000000000..17323246f --- /dev/null +++ b/.github/workflows/hardware-test.yml @@ -0,0 +1,63 @@ +name: Hardware Tests (GHA) + +# 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 + +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') + # 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: + 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. + # `[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[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" + --lg-env "$LG_ENV" + --iio-uri-override "$IIO_URI_OVERRIDE" + --junitxml="$JUNIT" + artifact_glob: | + junit-hw-*.xml + **/*.log + prism_project: pyadi-iio + secrets: inherit diff --git a/test/hw/README.md b/test/hw/README.md new file mode 100644 index 000000000..03811b95b --- /dev/null +++ b/test/hw/README.md @@ -0,0 +1,89 @@ +# 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 **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 +├── 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) 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 + +```bash +# Without labgrid (point at an iiod yourself) +pytest -v test/hw/test_ad9081_smoke.py --iio-uri-override ip:10.0.0.23 + +# 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 family + +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. +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 + +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 000000000..5cb6bb569 --- /dev/null +++ b/test/hw/conftest.py @@ -0,0 +1,55 @@ +"""HW-test conftest. + +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 pytest + + +def pytest_addoption(parser): + g = parser.getgroup("hw") + g.addoption( + "--iio-uri-override", + 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 the explicitly-named DUT URI. + + 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. + """ + uri = (request.config.getoption("--iio-uri-override") or "").strip() + if not uri: + pytest.skip( + "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 uri diff --git a/test/hw/test_ad9081_smoke.py b/test/hw/test_ad9081_smoke.py new file mode 100644 index 000000000..0283dd637 --- /dev/null +++ b/test/hw/test_ad9081_smoke.py @@ -0,0 +1,38 @@ +"""Smoke test: open an AD9081 over IIO and read its context. + +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) + try: + assert sdr.ctx is not None + assert sdr.ctx.attrs, f"empty context attrs at {iio_uri}" + finally: + 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) + 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_ad9371_smoke.py b/test/hw/test_ad9371_smoke.py new file mode 100644 index 000000000..fa3a15e00 --- /dev/null +++ b/test/hw/test_ad9371_smoke.py @@ -0,0 +1,38 @@ +"""Smoke test: open an AD9371 / ADRV9371 over IIO. + +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) + try: + assert sdr.ctx is not None + assert sdr.ctx.attrs, f"empty context attrs at {iio_uri}" + finally: + 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) + 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 000000000..f32ea1505 --- /dev/null +++ b/test/hw/test_adrv9009_smoke.py @@ -0,0 +1,37 @@ +"""Smoke test: open an ADRV9009 over IIO. + +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) + try: + assert sdr.ctx is not None + assert sdr.ctx.attrs, f"empty context attrs at {iio_uri}" + finally: + 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) + try: + sdr.rx_buffer_size = 1024 + data = sdr.rx() + assert data is not None + assert len(data) > 0 + finally: + del sdr