Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/scripts/bootstrap-uv.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Ensure `uv` is available in $HOME/.local/bin on the runner.
#
# Called at the start of every hw-test workflow step that needs uv.
# On a fresh runner this downloads the standalone installer (~15 MB);
# on subsequent runs it's a no-op. Exports PATH for the caller via
# $GITHUB_PATH so later steps in the same job see ~/.local/bin too.
#
# Mirror of pyadi-iio's .github/scripts/bootstrap-uv.sh — same labs,
# same convention.

set -euo pipefail

UV_BIN="$HOME/.local/bin/uv"

if [[ ! -x "$UV_BIN" ]]; then
echo "uv not found at $UV_BIN — installing via astral.sh" >&2
curl -LsSf https://astral.sh/uv/install.sh | sh
fi

echo "$HOME/.local/bin" >> "$GITHUB_PATH"
"$UV_BIN" --version
28 changes: 28 additions & 0 deletions .github/scripts/install-trx-hw-venv.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Install TransceiverToolbox HW-CI Python deps into a persistent
# uv-managed venv at ~/.cache/trx-hw-ci/venv on the current runner.
#
# Mirror of pyadi-iio's install-pyadi-iio-venv.sh, but our deps are
# only what's needed for the labgrid boot lifecycle + the MATLAB
# bridge tests — no pyadi-iio, no matplotlib, no scipy:
#
# adi-labgrid-plugins[kuiper] registers labgrid resource/driver
# classes the env yamls reference; the
# pytest11 plugin auto-registers and
# drives HW_DAUGHTER / HW_CARRIER
# marker filtering.
# pytest, pyyaml minimal test runner deps.

set -euo pipefail

VENV="$HOME/.cache/trx-hw-ci/venv"

export PATH="$HOME/.local/bin:$PATH"

if [[ ! -x "$VENV/bin/python" ]]; then
echo "Creating trx HW-CI venv at $VENV" >&2
uv venv --quiet "$VENV"
fi

uv pip install --quiet --python "$VENV/bin/python" \
-r test/hw/requirements_dev.txt
232 changes: 232 additions & 0 deletions .github/workflows/hardware-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
name: Hardware Tests

# Modelled on pyadi-iio's hardware-test.yml (same labs, same coordinator,
# same conventions). The shape is intentional: preflight probes the
# coordinator for places, intersects against `test/hw/env/*.yaml`
# committed in this repo, and emits a matrix; per-place shards acquire
# the place + run pytest + release. The bridge tests in test/hw/
# subprocess MATLAB once per supported board reference name.

permissions:
contents: read
# dorny/test-reporter creates Check Runs for the per-leg JUnit reports.
checks: write
pull-requests: write

on:
push:
branches: [master]
pull_request:
# Gated below by the `hw-test` label so PRs don't auto-grab the
# self-hosted runners.
types: [labeled, opened, synchronize, reopened]
workflow_dispatch:

env:
COORDINATOR: ${{ vars.ADI_LG_COORDINATOR }}

concurrency:
group: hw-${{ github.ref }}
cancel-in-progress: false

jobs:
preflight:
# Gate: on PRs only when the `hw-test` label is applied. Push/dispatch
# always run.
if: >-
github.event_name != 'pull_request' ||
contains(github.event.pull_request.labels.*.name, 'hw-test')
runs-on: [self-hosted, hw-coordinator]
timeout-minutes: 3
outputs:
available_nodes: ${{ steps.probe.outputs.available_nodes }}
steps:
- uses: actions/checkout@v4

- name: Bootstrap uv
run: bash .github/scripts/bootstrap-uv.sh

- id: probe
timeout-minutes: 2
env:
LABGRID_PIP: 'labgrid @ git+https://github.com/tfcollins/labgrid.git@tfcollins/plugin-support'
run: |
set -euo pipefail
HOST="${COORDINATOR%%:*}"
PORT="${COORDINATOR##*:}"
if ! timeout 10 bash -c "</dev/tcp/$HOST/$PORT" 2>/dev/null; then
echo "Coordinator $COORDINATOR unreachable — marking all nodes unavailable." >&2
echo 'available_nodes=[]' >> "$GITHUB_OUTPUT"
exit 0
fi
export PATH="$HOME/.local/bin:$PATH"
VENV="$HOME/.cache/trx-hw-ci/labgrid-venv"
if [[ ! -x "$VENV/bin/labgrid-client" ]]; then
echo "Creating labgrid venv at $VENV" >&2
uv venv --quiet "$VENV"
uv pip install --quiet --python "$VENV/bin/python" "$LABGRID_PIP"
fi
LGCLIENT="$VENV/bin/labgrid-client"
echo "Using labgrid-client: $LGCLIENT" >&2
echo "--- places -v output ---" >&2
PLACES_OUT=$(timeout 30 "$LGCLIENT" -x "$COORDINATOR" -v places 2>&1 || true)
echo "$PLACES_OUT" >&2
echo "--- end places -v output ---" >&2
python3 - "$PLACES_OUT" >> "$GITHUB_OUTPUT" <<'PY'
import json, os, re, sys
# Discovery-driven matrix (same pattern pyadi-iio uses):
# 1. Coordinator publishes places, each tagged with
# carrier=<X> / daughter-board=<Y>.
# 2. Per-place env yamls live at test/hw/env/<place>.yaml.
# A place is auto-included when its yaml exists in the
# repo.
# 3. We also emit the place's `daughter-board` tag so the
# shard can set $HW_DAUGHTER for the adi_lg_plugins
# pytest plugin's iio_hardware-marker filter.
place_re = re.compile(r"^Place '(?P<name>[^']+)':")
tags_re = re.compile(r"^\s+tags:\s+(?P<tags>.*)$")
places = {}
current = None
for line in sys.argv[1].splitlines():
m = place_re.match(line)
if m:
current = m.group("name")
places[current] = {}
continue
if current is None:
continue
m = tags_re.match(line)
if m:
for kv in m.group("tags").split(","):
kv = kv.strip()
if "=" in kv:
k, v = kv.split("=", 1)
places[current][k.strip()] = v.strip()

repo_root = os.environ.get("GITHUB_WORKSPACE", ".")
avail = []
for name, tags in sorted(places.items()):
env_yaml = f"test/hw/env/{name}.yaml"
if os.path.exists(os.path.join(repo_root, env_yaml)):
avail.append({
"place": name,
"env_remote": env_yaml,
"daughter_board": tags.get("daughter-board", ""),
"carrier": tags.get("carrier", ""),
})
else:
print(
f"skip: place {name!r} (tags={tags}) — {env_yaml} not in repo",
file=sys.stderr,
)
print("available_nodes=" + json.dumps(avail))
PY

hw-coord:
needs: preflight
if: needs.preflight.outputs.available_nodes != '[]'
strategy:
fail-fast: false
matrix:
node: ${{ fromJSON(needs.preflight.outputs.available_nodes) }}
name: hw-coord (${{ matrix.node.place }})
# Per-place runner. `hw-<place>` is the convention from
# adi-labgrid-plugins; the host running the runner must have MATLAB,
# ssh access to lab hosts as needed, and reach the coordinator.
runs-on: [self-hosted, "hw-${{ matrix.node.place }}"]
timeout-minutes: 40
concurrency:
group: hw-coord-${{ matrix.node.place }}
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
with:
# +adi/+common is a git submodule (ToolboxCommon, shared
# across ADI MATLAB toolboxes); MATLAB can't resolve
# adi.common.RxTx without it.
submodules: recursive

- name: Bootstrap uv
run: bash .github/scripts/bootstrap-uv.sh

- name: Install trx HW venv (adi-labgrid-plugins[kuiper] + pytest)
run: bash .github/scripts/install-trx-hw-venv.sh

- name: Acquire coordinator place
env:
LG_COORDINATOR: ${{ env.COORDINATOR }}
run: |
set -euo pipefail
LGCLIENT="$HOME/.cache/trx-hw-ci/venv/bin/labgrid-client"
"$LGCLIENT" -x "$LG_COORDINATOR" -p "${{ matrix.node.place }}" acquire

- name: Run coordinator-mode tests
env:
LG_COORDINATOR: ${{ env.COORDINATOR }}
LG_ENV: ${{ github.workspace }}/${{ matrix.node.env_remote }}
# HW_DAUGHTER drives adi_lg_plugins.pytest_plugin's
# iio_hardware-marker filter: tests whose
# @pytest.mark.iio_hardware([...]) doesn't include this chip
# are auto-skipped.
HW_DAUGHTER: ${{ matrix.node.daughter_board }}
HW_CARRIER: ${{ matrix.node.carrier }}
# MATLAB binary; override per-lab via vars.MATLAB_BIN.
MATLAB_BIN: ${{ vars.MATLAB_BIN || 'matlab' }}
run: |
set -euo pipefail
# The bridge tests in test/hw/test_*.py subprocess MATLAB.
# Bridge skips when iio_uri unresolved (conftest no-op);
# marker filter skips tests whose chip != $HW_DAUGHTER.
"$HOME/.cache/trx-hw-ci/venv/bin/pytest" \
-v -s -p no:cacheprovider \
test/hw/ \
--junitxml=junit-hw-coord-${{ matrix.node.place }}.xml

- name: Release coordinator place
if: always()
env:
LG_COORDINATOR: ${{ env.COORDINATOR }}
run: |
set -euo pipefail
LGCLIENT="$HOME/.cache/trx-hw-ci/venv/bin/labgrid-client"
"$LGCLIENT" -x "$LG_COORDINATOR" -p "${{ matrix.node.place }}" release || true

- name: Publish hw-coord test results to PR
if: always()
uses: dorny/test-reporter@v1
with:
name: Tests (hw-coord ${{ matrix.node.place }})
path: junit-hw-coord-${{ matrix.node.place }}.xml
reporter: java-junit
fail-on-error: false

- name: Upload JUnit + MATLAB artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: junit-hw-coord-${{ matrix.node.place }}
path: |
junit-hw-coord-${{ matrix.node.place }}.xml
${{ matrix.node.daughter_board && format('*{0}*_HWTestResults.xml', matrix.node.daughter_board) || '*_HWTestResults.xml' }}
failures.txt
retention-days: 14
if-no-files-found: ignore

publish-pr-test-summary:
name: PR Test Summary
needs: [hw-coord]
if: always() && github.event_name == 'pull_request' && needs.hw-coord.result != 'skipped'
runs-on: [self-hosted, hw-coordinator]
steps:
- uses: actions/download-artifact@v4
with:
pattern: junit-hw-*
path: junit
merge-multiple: true

- uses: EnricoMi/publish-unit-test-result-action@v2
with:
files: 'junit/**/*.xml'
check_name: 'Hardware Test Results'
comment_mode: always
report_individual_runs: true
Loading
Loading