Skip to content

Commit 368a198

Browse files
josephnefclaude
andcommitted
tests: --full-matrix runs N-adapter cross-driver interop tables
Extends tests/regress.py with a --full-matrix mode that iterates every ordered (TX, RX) pair of plugged-in DUTs across all four driver-side combinations (kernel-only, devourer-TX/kernel-RX, kernel-TX/devourer-RX, devourer-only) and emits one NxN table per mode instead of one 4-cell table for a single pair. Useful for catching cross-chipset interop regressions in PRs that touch shared HAL code. Usage: sudo python3 tests/regress.py --full-matrix --channel 100 \\ --vm-name devourer-testrig --vm-ssh <user>@<VM-IP> For N adapters, runs N*(N-1)*4 cells total — at ~30-40s per cell in VM mode that's ~16 min for N=3, manageable. Diagonal is blanked (same physical adapter can't simultaneously TX and RX with one driver). The script reuses run_cell as-is; the addition is just the outer pair loop, result dict keyed by (tx_side, rx_side, tx_vidpid, rx_vidpid), and a new emit_full_markdown that renders four NxN tables. Also scrubs personal identifiers from earlier docs/scripts (PR #33): - tests/setup_vm.sh now reads VM_USER from $SUDO_USER / $USER instead of hardcoding a specific username - tests/README.md + regress.py docstrings switch to <user>@<VM-IP> placeholders in example commands Validation on a 3-adapter rig (Ubuntu 22.04 VM with aircrack-ng/88XXau, 0bda:8812 + 0bda:8813 + 2357:0120, channel 100, 10s/cell): ## Kernel-only (rig sanity) All 6 cross-chipset cells pass — 88XXau handles all three chipsets cleanly in the pinned-kernel VM (88-271 hits per cell). ## devourer-TX → kernel-RX devourer-TX confirmed for 8812 (4114, 4693 hits) AND 8821 (4341 hits reaching 8814 kernel RX). 8814 TX flaky after passthrough cycles (chip-state degradation across cell sequencing — known sensitive). ## kernel-TX → devourer-RX Surprise — devourer-RX 8821 caught 200 frames from kernel-TX 8814, contradicting PR #30's "RX silent" finding. devourer-RX 8812 confirmed (100 hits from each of 8814, 8821 TX). devourer-RX 8814 confirmed broken (0 hits all directions — known TODO). ## devourer ↔ devourer All 0 — every cell hits at least one broken side (8814 RX or 8814 TX degraded mid-run). Net new product signal from the full matrix: - devourer-TX 8821 actually works (was unvalidated since PR #30 had no peer sniffer in that session — VM mode is the peer) - devourer-RX 8821 works under at least one TX condition — reopen PR #30's "RX silent" conclusion - 8814 chip state degrades through repeated host↔VM passthrough — needs investigation, may want a chip reset between cells 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 73a29e2 commit 368a198

3 files changed

Lines changed: 165 additions & 12 deletions

File tree

tests/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ Then run the matrix in VM mode:
5050
```bash
5151
sudo python3 tests/regress.py --channel 100 \
5252
--vm-name devourer-testrig \
53-
--vm-ssh dima@<VM-IP-from-status>
53+
--vm-ssh <user>@<VM-IP-from-status>
5454
```
5555

5656
VM mode is what unblocks chipsets where the host kernel driver doesn't
@@ -83,7 +83,8 @@ probe on kernels 6.15+ (`failed to download firmware`, `error -22`), but
8383
(download from <https://cloud-images.ubuntu.com/jammy/current/>)
8484
- Working USB hot-plug on libvirt (`xhci` controller; `setup_vm.sh` adds it)
8585
- The host user's SSH key in `~/.ssh/id_rsa.pub` (or set `SSH_PUBKEY=...`
86-
before `setup_vm.sh`) — gets baked into the VM's `dima` user
86+
before `setup_vm.sh`) — gets baked into the VM's user account
87+
(defaults to your invoking user; override with `VM_USER=foo`)
8788

8889
The script does a preflight check and prints distro-agnostic install
8990
hints for anything missing.
@@ -97,7 +98,7 @@ Markdown table to stdout, ready to paste into PR comments:
9798
9899
- TX adapter: `0bda:8812` (RTL8812AU)
99100
- RX adapter: `0bda:8813` (RTL8814AU)
100-
- Kernel host: VM devourer-testrig via dima@10.216.129.126
101+
- Kernel host: VM devourer-testrig via <user>@<VM-IP>
101102
- Cell duration: 10s
102103
- Pass threshold: ≥ 3 hits
103104

tests/regress.py

Lines changed: 151 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
sudo python3 tests/regress.py --channel 100
3737
# VM mode (after tests/setup_vm.sh):
3838
sudo python3 tests/regress.py --channel 100 \\
39-
--vm-name devourer-testrig --vm-ssh dima@10.216.129.126
39+
--vm-name devourer-testrig --vm-ssh <user>@<VM-IP>
4040
4141
Portability: tool paths resolved via `which`, wlan interfaces discovered via
4242
`iw dev` (works for systemd `wlp*` and classic `wlan*`), kernel driver
@@ -115,7 +115,7 @@ def run(cmd: list[str], **kw) -> subprocess.CompletedProcess:
115115
class KernelHost:
116116
"""One of two flavours. Use KernelHost.local() or KernelHost.via_ssh()."""
117117

118-
# ssh target like "dima@10.216.129.126". Empty string for local execution.
118+
# ssh target like "<user>@<vm-ip>". Empty string for local execution.
119119
ssh_target: str = ""
120120
# libvirt domain name for USB passthrough. Empty for local mode (no DUT
121121
# movement needed — DUTs already on the same machine).
@@ -843,6 +843,110 @@ def run_matrix(
843843
return results
844844

845845

846+
# ---------------------------------------------------------------------------
847+
# N-adapter full matrix — runs every ordered (TX, RX) pair across all 4
848+
# driver-side combinations and emits one NxN table per mode.
849+
# ---------------------------------------------------------------------------
850+
851+
852+
# The four mode-matrices. Each is a (tx_side, rx_side) tuple labelled with
853+
# the question it answers.
854+
FULL_MATRIX_MODES = [
855+
("kernel", "kernel",
856+
"Kernel-only (rig sanity / cross-chipset kernel interop)"),
857+
("devourer", "kernel",
858+
"devourer TX → kernel RX (does devourer emit valid frames?)"),
859+
("kernel", "devourer",
860+
"kernel TX → devourer RX (does devourer RX a known-good frame?)"),
861+
("devourer", "devourer",
862+
"devourer ↔ devourer (end-to-end devourer)"),
863+
]
864+
865+
866+
def run_full_matrix(
867+
devourer_root: Path,
868+
duts: list[Dut],
869+
channel: int,
870+
duration: float,
871+
threshold: int,
872+
tmpdir: Path,
873+
kh: KernelHost,
874+
) -> dict[tuple[str, str, str, str], CellResult]:
875+
"""Run every ordered (TX, RX) pair of distinct DUTs across all four
876+
driver-side combinations. Returns a dict keyed by
877+
(tx_side, rx_side, tx_vidpid, rx_vidpid)."""
878+
results: dict[tuple[str, str, str, str], CellResult] = {}
879+
pairs = [(tx, rx) for tx in duts for rx in duts if tx.sysfs_id != rx.sysfs_id]
880+
total = len(pairs) * len(FULL_MATRIX_MODES)
881+
idx = 0
882+
for tx_dut, rx_dut in pairs:
883+
for tx_side, rx_side, _label in FULL_MATRIX_MODES:
884+
idx += 1
885+
cell_id = (
886+
f"[{time.strftime('%H:%M:%S')}] [{idx}/{total}] "
887+
f"TX={tx_dut.chipset} ({tx_side}) → "
888+
f"RX={rx_dut.chipset} ({rx_side})"
889+
)
890+
print(cell_id + " ...", flush=True)
891+
try:
892+
r = run_cell(
893+
devourer_root, tx_dut, rx_dut, tx_side, rx_side,
894+
channel, duration, tmpdir, kh,
895+
)
896+
except Exception as e:
897+
print(f" ✗ cell crashed: {e}", flush=True)
898+
r = CellResult(hits=0, tx_attempts=0, tx_failures=0,
899+
duration_s=0.0, notes=str(e))
900+
results[(tx_side, rx_side, tx_dut.vidpid, rx_dut.vidpid)] = r
901+
print(f" → {r.fmt(threshold)}", flush=True)
902+
return results
903+
904+
905+
def emit_full_markdown(
906+
duts: list[Dut],
907+
channel: int,
908+
duration: float,
909+
threshold: int,
910+
kh: KernelHost,
911+
results: dict[tuple[str, str, str, str], CellResult],
912+
) -> str:
913+
"""Render four NxN tables, one per (tx_side, rx_side) mode. Diagonal is
914+
blanked (can't TX and RX with the same physical adapter)."""
915+
out = []
916+
out.append(f"# Full regression matrix — channel {channel}, "
917+
f"{time.strftime('%Y-%m-%d %H:%M:%S')}\n")
918+
out.append(f"- Kernel host: "
919+
f"{'VM ' + kh.vm_name + ' via ' + kh.ssh_target if kh.is_remote else 'local'}")
920+
out.append(f"- Cell duration: {duration:.0f}s Pass threshold: ≥ {threshold} hits")
921+
out.append("- Adapters:")
922+
for d in duts:
923+
out.append(f" - `{d.vidpid}` ({d.chipset})")
924+
out.append("")
925+
926+
short = {d.vidpid: d.chipset.split(" ")[0] for d in duts}
927+
928+
for tx_side, rx_side, label in FULL_MATRIX_MODES:
929+
out.append(f"## {label}\n")
930+
# Header
931+
header = "| TX \\ RX |" + "".join(
932+
f" {short[d.vidpid]} |" for d in duts
933+
)
934+
sep = "|---|" + "---|" * len(duts)
935+
out.append(header)
936+
out.append(sep)
937+
for tx_dut in duts:
938+
row_cells = []
939+
for rx_dut in duts:
940+
if tx_dut.sysfs_id == rx_dut.sysfs_id:
941+
row_cells.append("—")
942+
continue
943+
r = results.get((tx_side, rx_side, tx_dut.vidpid, rx_dut.vidpid))
944+
row_cells.append(r.fmt(threshold) if r else "?")
945+
out.append(f"| {short[tx_dut.vidpid]} | " + " | ".join(row_cells) + " |")
946+
out.append("")
947+
return "\n".join(out)
948+
949+
846950
def emit_markdown(
847951
tx_dut: Dut, rx_dut: Dut, channel: int, duration: float,
848952
threshold: int, kh: KernelHost,
@@ -912,6 +1016,12 @@ def main():
9121016
"--no-baseline-abort", action="store_true",
9131017
help="run all 4 cells even if kernel-kernel baseline fails",
9141018
)
1019+
ap.add_argument(
1020+
"--full-matrix", action="store_true",
1021+
help="iterate every ordered (TX, RX) pair of plugged DUTs across "
1022+
"all 4 driver-side combinations. Emits four NxN tables instead "
1023+
"of one 4-cell table. Ignores --tx-pid / --rx-pid.",
1024+
)
9151025
ap.add_argument(
9161026
"--vm-name",
9171027
default=os.environ.get("DEVOURER_VM_NAME", ""),
@@ -959,6 +1069,45 @@ def pick(pid_arg, default_idx):
9591069
sys.stderr.write(f"No plugged DUT has PID {pid_arg}\n")
9601070
sys.exit(2)
9611071

1072+
if args.full_matrix:
1073+
print(f"Full matrix mode over {len(duts)} adapters:")
1074+
for d in duts:
1075+
print(f" - {d.vidpid} ({d.chipset}) at {d.sysfs_id}")
1076+
print(f"Kernel host: "
1077+
f"{'VM ' + kh.vm_name + ' (' + kh.ssh_target + ')' if kh.is_remote else 'local'}")
1078+
n_pairs = len(duts) * (len(duts) - 1)
1079+
n_cells = n_pairs * len(FULL_MATRIX_MODES)
1080+
print(f"Channel: {args.channel} Duration/cell: {args.duration}s "
1081+
f"Pass threshold: ≥{args.pass_threshold} hits")
1082+
print(f"Total cells: {n_cells} "
1083+
f"({n_pairs} ordered pairs × {len(FULL_MATRIX_MODES)} mode-combos)\n")
1084+
1085+
kh.release_all_known_duts(duts)
1086+
1087+
with tempfile.TemporaryDirectory(prefix="devourer-regress-") as td:
1088+
tmpdir = Path(td)
1089+
results = run_full_matrix(
1090+
devourer_root=args.devourer_root,
1091+
duts=duts,
1092+
channel=args.channel, duration=args.duration,
1093+
threshold=args.pass_threshold,
1094+
tmpdir=tmpdir, kh=kh,
1095+
)
1096+
print()
1097+
md = emit_full_markdown(
1098+
duts, args.channel, args.duration,
1099+
args.pass_threshold, kh, results,
1100+
)
1101+
print(md)
1102+
if args.keep_logs:
1103+
kept = Path(tempfile.gettempdir()) / "devourer-regress-last"
1104+
if kept.is_symlink() or kept.exists():
1105+
kept.unlink()
1106+
kept.symlink_to(tmpdir)
1107+
print(f"(logs kept at {kept} — symlink, valid until next run)")
1108+
os._exit(0)
1109+
return
1110+
9621111
tx_dut = pick(args.tx_pid, 0)
9631112
rx_dut = pick(args.rx_pid, 1)
9641113
if tx_dut.sysfs_id == rx_dut.sysfs_id:

tests/setup_vm.sh

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ VM_VCPUS="${VM_VCPUS:-2}"
3030
VM_DISK_GB="${VM_DISK_GB:-20}"
3131
BASE_IMAGE="${BASE_IMAGE:-/var/lib/libvirt/images/jammy-base.qcow2}"
3232
LIBVIRT_IMAGES="${LIBVIRT_IMAGES:-/var/lib/libvirt/images}"
33-
SSH_PUBKEY="${SSH_PUBKEY:-$HOME/.ssh/id_rsa.pub}"
34-
WORK_DIR="${WORK_DIR:-$HOME/devourer-testrig-setup}"
33+
# Username to create inside the VM. Defaults to the invoking user
34+
# (SUDO_USER when called via sudo, else USER). Override with VM_USER=foo.
35+
VM_USER="${VM_USER:-${SUDO_USER:-$USER}}"
36+
SSH_PUBKEY="${SSH_PUBKEY:-$(eval echo "~$VM_USER/.ssh/id_rsa.pub")}"
37+
WORK_DIR="${WORK_DIR:-$(eval echo "~$VM_USER/devourer-testrig-setup")}"
3538

3639
cmd="${1:-provision}"
3740

@@ -53,7 +56,7 @@ case "$cmd" in
5356
ip=$(vm_ip)
5457
echo "IP: ${ip:-(none — DHCP not assigned)}"
5558
if [ -n "${ip:-}" ]; then
56-
echo "SSH: ssh dima@$ip"
59+
echo "SSH: ssh $VM_USER@$ip"
5760
fi
5861
echo "USB passthrough (current):"
5962
sudo virsh dumpxml "$VM_NAME" 2>/dev/null \
@@ -91,7 +94,7 @@ hostname: $VM_NAME
9194
manage_etc_hosts: true
9295
9396
users:
94-
- name: dima
97+
- name: $VM_USER
9598
sudo: ALL=(ALL) NOPASSWD:ALL
9699
shell: /bin/bash
97100
ssh_authorized_keys:
@@ -166,14 +169,14 @@ fi
166169
echo "waiting for cloud-init to finish (installs aircrack-ng driver, ~5-10 min)..."
167170
ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=5 \
168171
-o UserKnownHostsFile=/dev/null \
169-
dima@"$ip" "cloud-init status --wait" 2>&1 | tail -3
172+
$VM_USER@"$ip" "cloud-init status --wait" 2>&1 | tail -3
170173

171174
echo
172175
echo "=== VM ready ==="
173-
echo "ssh dima@$ip"
176+
echo "ssh $VM_USER@$ip"
174177
echo
175178
echo "Verify aircrack-ng driver:"
176-
echo " ssh dima@$ip 'sudo modprobe 88XXau && lsmod | grep 88XXau'"
179+
echo " ssh $VM_USER@$ip 'sudo modprobe 88XXau && lsmod | grep 88XXau'"
177180
echo
178181
echo "Hot-plug a DUT into the VM (example for 8814AU):"
179182
echo " cat > /tmp/usb-8814.xml << 'XML'"

0 commit comments

Comments
 (0)