Skip to content

ci: add labgrid-coordinator-driven hardware tests - #744

Open
tfcollins wants to merge 19 commits into
mainfrom
tfcollins/hw-coord-ci
Open

ci: add labgrid-coordinator-driven hardware tests#744
tfcollins wants to merge 19 commits into
mainfrom
tfcollins/hw-coord-ci

Conversation

@tfcollins

Copy link
Copy Markdown
Collaborator

Summary

New self-hosted-runner workflow that, per labgrid place exposed by the coordinator (10.0.0.41:20408), acquires the place, fresh-boots via the labgrid Strategy, runs marker-selected pytest, and releases. Each leg posts a per-place check run; a final job aggregates JUnit results into a single PR comment.

  • test/conftest.py override (gated by LG_ENV) provides iio_uri from labgrid.NetworkService.address. When LG_ENV is unset the existing emu/unit-test path is untouched.
  • Per-place env yamls under test/hw/env/{bq,mini2,nuc}.yaml describe targets/drivers/boot strategy.
  • .github/hw-nodes.json maps places to pytest markers (one entry per place).
  • Helper scripts: bootstrap-uv.sh, install-pyadi-iio-venv.sh, register-hw-runners.sh (single local runner, mirrored from pyadi-dt's pattern but simplified — pyadi-iio's tests don't need per-board Vivado-equipped runners).
  • Fork PRs gated by the existing external-pr GitHub environment (matches iio-context-coverage.yml).

Test plan

  • Preflight job emits a non-empty available_nodes list (or empty if coordinator unreachable; either is graceful).
  • Each available place's hw-coord matrix leg acquires, runs pytest -m <marker>, and releases.
  • dorny/test-reporter posts a per-place check on the PR.
  • EnricoMi/publish-unit-test-result-action posts an aggregated comment.
  • Marker _LABGRID_MODE=False path: existing Test-Emulated-HW workflow continues to pass unchanged on this PR.

🤖 Generated with Claude Code

New self-hosted-runner workflow runs marker-selected pytest legs against
real hardware via the labgrid coordinator (10.0.0.41:20408), one matrix
job per available place. Each leg acquires the place, fresh-boots via
the labgrid Strategy, runs tests, and releases.

- test/conftest.py override (gated by LG_ENV) provides iio_uri from
  labgrid NetworkService.address; emu/unit path is untouched when LG_ENV
  is unset.
- Per-place env yamls under test/hw/env/ describe targets/drivers/boot.
- .github/hw-nodes.json maps places to markers (one entry per place).
- Helper scripts: bootstrap-uv.sh, install-pyadi-iio-venv.sh,
  register-hw-runners.sh.
- Workflow gated by external-pr environment for fork PRs (mirrors
  iio-context-coverage.yml's pattern).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test/conftest.py imports test.rf.spec which pulls in scipy via
scipy.signal.windows.kaiser; pyadi-iio[jesd] alone is not enough.
Mirror the existing emu-CI install_pydeps.sh pattern by installing
requirements_dev.txt into the labgrid runner's venv.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without adi-labgrid-plugins, the coordinator-forwarded resources
(KuiperRelease, VesyncPowerDriver, KuiperDLDriver, etc.) fail to
deserialize on the runner side with InvalidConfigError. The plugin
package transitively pulls in the tfcollins/plugin-support labgrid
fork, replacing the explicit LABGRID_PIP entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown

Hardware Test Results

    2 files    2 suites   52s ⏱️
1 842 tests 103 ✅ 1 739 💤 0 ❌
3 684 runs  141 ✅ 3 543 💤 0 ❌

Results for commit 34eee7b.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown

Generated documentation for this PR is available at Link

The package source repo is tfcollins/labgrid-plugins (public), not
analogdevicesinc/adi-labgrid-plugins (does not exist). Previous URL
was invented from the package name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown

Test Results

1 812 tests  ±0     507 ✅  - 1   4m 18s ⏱️ -5s
    1 suites ±0   1 305 💤 +1 
    1 files   ±0       0 ❌ ±0 

Results for commit 34eee7b. ± Comparison against base commit ee07c9b.

This pull request removes 1 and adds 1 tests. Note that renamed tests count towards both.
test.test_ad9081 ‑ test_ad9081_attr[attr=tx_main_ffh_index-start=1-stop=31-step=1-tol=0-repeats=10-classname=adi.ad9081]
test.test_ad9081 ‑ test_ad9081_attr[attr=tx_main_ffh_index-start=0-stop=30-step=1-tol=0-repeats=10-classname=adi.ad9081]
This pull request skips 1 test.
test.test_ad9081 ‑ test_ad9081_loopback[channel=0-classname=adi.ad9081]

♻️ This comment has been updated with latest results.

bq + mini2 fail at strategy construction because BootFPGASoC[TFTP]
calls KuiperDLDriver.get_boot_files_from_release() during
__attrs_post_init__, which transitively imports pytsk3. pytsk3 is in
the [kuiper] extra of adi-labgrid-plugins; manylinux wheels exist so
no libtsk-dev install is required.

nuc needs the runner host (nemo) to SSH-key-auth into nuc itself for
XilinxJTAGDriver._run_xsdb's scp call. That key isn't yet provisioned;
drop nuc from the manifest until it is. preflight will simply not
emit a matrix leg for nuc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bq.yaml's TFTPServerDriver runs on the runner host (nemo) and serves
boot files to the bq board via auto-detected runner IP. Default root
/var/lib/tftpboot isn't writable by the runner user; switch to
/tmp/pyadi-iio-tftpboot which the driver auto-creates.

Restoring nuc to the manifest now that SSH key auth from nemo to each
lab host (bq, mini2, nuc) will be provisioned out-of-band via
ssh-copy-id. The labgrid drivers (USBSDMuxDriver on mini2,
XilinxJTAGDriver on nuc, etc.) ssh/scp from the runner into the host
that owns each physical resource, so key auth nemo->{bq,mini2,nuc} is
load-bearing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mini2 regressed on the prior run because shell.run() captured stderr
into stdout — when the kernel hadn't brought up networking yet,
'RTNETLINK answers: Network is unreachable' became the 'IP', and the
fixture happily built ip:RTNETLINK... and then failed iio.Context.
Wrap the IP read in a 60s poll loop and validate the line is dotted-
quad before accepting it.

Drop bq from the place manifest. The Kuiper release's adrv9371
uImage/devicetree.dtb (5c9e342a... / 30KB dtb) don't match what bq's
known-working bootflow uses (2545d6f9... / 82KB dtb) — different
kernel build, different DTS. pyadi-dt builds these locally per-run
on the bq host; pyadi-iio's runner-on-nemo path can't easily mirror
that without either an rsync hop from bq or a build-from-source
flow. Re-add when one of those is in place.

The bq exporter config changes (kernel_path, device_tree_path) stay
applied so when this is revisited, the resource side is ready.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pytest_collection_modifyitems treated a single-string iio_hardware
arg as a sequence and used `key in hardware_list` for matching,
which silently became a string-substring check:

    @pytest.mark.iio_hardware("ad9081_full_bw")
    -> "ad9081" in "ad9081_full_bw"  # True (substring!)

That made test_full_bw_rx pick up every marker from
test_map["ad9081"] (incl. zynqmp_zcu102_rev10_ad9081_m8_l4_vcxo122p88)
and be selected by `-m <board-marker>` runs of unrelated configs.

Wrap a single string in a list so the `in` check is element
equality.  Tests with a list-form @iio_hardware were already correct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wrap the iio_uri yield in try/finally and transition the strategy to
'powered_off' on teardown. All three strategies in current use
(BootFPGASoC, BootFPGASoCTFTP, BootFabric) define Status.powered_off
so a single transition string works across legs.

Cleanup errors are caught + warned, never re-raised, so a flaky
power-off can't mask a real test failure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the manifest-pinned `-m <board-marker>` selection model with
pytest-libiio's built-in discovery: pytest_configure boots the lab
board, polls for the DHCP-assigned IP and IIO daemon readiness, and
sets config.option.uri.  pytest-libiio's _contexts fixture then scans
that URI, identifies the hardware against
test/emu/hardware_map.yml's existing entries, and skips any test
whose @iio_hardware(...) decorator doesn't match the discovered
hardware identifier.

Adding a new place is now {place, env_remote} in hw-nodes.json plus a
new env yaml — no marker math, no test_map.py cross-reference, and
bitstream/dtb variant drift no longer requires manifest edits.

- test/conftest.py: drop the labgrid_iio_uri/iio_uri override
  fixtures; introduce pytest_configure (boot + set --uri) and
  pytest_sessionfinish (powered_off teardown).  The dotted-quad poll
  and iio.Context retry are lifted into _wait_for_ipv4 /
  _wait_for_iio helpers.
- .github/workflows/hardware-test.yml: drop -m and --skip-scan from
  the pytest invocation; pytest-libiio now handles selection.
- .github/hw-nodes.json: drop the marker field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two pre-existing test bugs that were silently filtered out by the
old pinned-marker selection model and now surface under
discovery-driven CI on AD9081 hardware:

1. test_ad9081_attr[attr=tx_main_ffh_index] used start=1, stop=31 but
   the AD9081 driver documents the valid range as [0, 30]
   (adi/ad9081.py:527). Random pick of val=31 is rejected, readback
   differs, and tol=0 trips the assert. Change parametrize to
   (0, 30, 1, 0, 10) to match the driver's documented range.

2. test_ad9081_loopback (and test_dac_zeros) call sdr.loopback = 1,
   which on AD9081 silently creates a no-op instance attribute (the
   class has loopback_mode for RX->TX, not loopback for TX->RX).
   Internal loopback is never enabled, RX captures ambient/random
   data, and the bit-exact comparison fails. Skip the test when the
   device class doesn't define a `loopback` property; hasattr()
   before any assignment is the reliable pre-write check (Python's
   default __setattr__ would otherwise mask the absence).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The lab-side path has multiple sources of transient flakes:
- mDNS resolution of mini2/nuc for rfc2217 serial URLs
  (socket.gaierror during SerialDriver.open)
- SSH ControlMaster bring-up to exporter hosts
- Stale serial console state from a prior aborted run

Each manifests as a hard error during strategy.transition('shell')
or Environment construction, but a second attempt 10s later
typically succeeds.

Wrap pytest_configure's labgrid boot in a 3-attempt retry; only
fail the session if every attempt errors.  Successful boots return
on attempt 1 with no observable change in cost.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tfcollins and others added 3 commits May 10, 2026 07:38
Preflight now parses `labgrid-client -v places` to read each place's
carrier= / daughter-board= tags, then includes the place in the
matrix iff a corresponding test/hw/env/<place>.yaml exists in the
repo.  Adding a new lab board becomes:

  1. tag the place via labgrid-client -p <name> set-tags carrier=...
  2. drop test/hw/env/<name>.yaml in this repo

No manifest edit needed.  hw-nodes.json is kept as an optional
override layer (currently empty) for places that need a non-default
yaml or that lack the carrier/daughter-board tags.

bq is currently auto-discoverable (tags exist) but its boot
artifacts in the Kuiper release don't match the working bq
bootflow.  Remove test/hw/env/bq.yaml so auto-discovery skips it
until the artifact path is sorted; restore from git history when
ready.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Microblaze IIO daemon on nuc occasionally drops connections under
load, surfacing as BrokenPipeError on iio.Context() construction or
attribute reads.  Pattern is intermittent — sometimes one test, last
run took out 17 — but always with the exact same BrokenPipeError
signature.

Add pytest-rerunfailures to the runner venv and pass:
  --reruns 2 --reruns-delay 5 --only-rerun "BrokenPipeError"

The --only-rerun filter is a regex against the failure message, so
real assertion failures (peak_min, attribute mismatch, etc.) stay
visible — only the daemon-drop class of failure gets retried.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pytest-rerunfailures 16.0 introduced a subprocess-based "rerun
server" that segfaults during pytest_runtest_protocol when the
session has loaded the libiio C extension (and probably labgrid's
pytest plugin too):

  Fatal Python error: Segmentation fault
  File ".../pytest_rerunfailures.py", line 466 in run_server
  File ".../pytest_rerunfailures.py", line 593 in pytest_runtest_protocol

v15 uses the simpler in-process rerun loop which doesn't fork and
doesn't cross any C-extension boundaries.  Pin to <16 until upstream
fixes the server-mode crash.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant