Skip to content

Commit f8cb171

Browse files
committed
test(e2e): add real-ABI DRDY test with spidev_test, gpio-sim and gpiomon
Covers sensor timer -> router -> GPIO runtime -> gpio-sim -> gpiomon edge -> SPI read -> read-clear -> falling edge, plus re-arm and negative cases. Includes a QEMU runner for hosts whose kernel lacks gpio-sim, cuse and configfs.
1 parent 5335405 commit f8cb171

4 files changed

Lines changed: 239 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11+
- `tests/e2e/drdy`: real-ABI end-to-end test (spidev_test, gpio-sim, gpiomon) for
12+
the signal topology flow, with a QEMU runner.
1113
- Public device signal ports, `signal_bindings`, and a `topology.yaml` that
1214
connects them to GPIO lines with deterministic zero-delay or `delay_ns`
1315
propagation (ADR 0011).
@@ -17,6 +19,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1719
- Separate CI jobs for Rust, web UI, native adapters (with ASan/UBSan) and examples.
1820
- CodeQL analysis, Dependabot configuration and a security policy.
1921

22+
### Fixed
23+
- `vds4e-spi-cuse` implements `SPI_IOC_RD_MODE32` and `SPI_IOC_WR_MODE32`, so the
24+
upstream `spidev_test` works.
25+
- Web UI lint errors in the waveform feature fixed at their source.
26+
- The live-server signal pump now logs the device events it applies and reports a
27+
persistent failure once.
28+
2029
### Changed
2130
- `uart-pty` adapter builds the shared bridge via `add_subdirectory`.
2231
- Workspace `rust-version` aligned with the pinned 1.91.1 toolchain; repository metadata corrected.

‎tests/e2e/drdy/README.md‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# DRDY end-to-end test (ADR 0011)
2+
3+
Exercises the full Linux ABI path with unmodified tools:
4+
5+
```text
6+
spi-sensor-drdy model (1 ms conversion timer)
7+
-> signal router (topology.yaml: spi-sensor-drdy.drdy -> generic-gpio-bank-32.GPIO16)
8+
-> generic GPIO runtime -> vds4e-gpio-sim -> kernel gpio-sim -> /dev/gpiochipN
9+
-> gpiomon (rising edge) -> upstream spidev_test read of /dev/spidev0.0
10+
-> read-clear -> gpiomon (falling edge)
11+
```
12+
13+
It also checks that DRDY does not clear before the sample is read, that it re-arms
14+
for a second conversion, and that a read without a new sample does not raise it.
15+
16+
This is a privileged ABI test and is not part of `cargo test` or the default CI.
17+
18+
## Run on a suitable host
19+
20+
Root, the `gpio-sim` and `cuse` modules, configfs, libgpiod tools, and an upstream
21+
`spidev_test` (build `tools/spi/spidev_test.c` from the Linux source) are required.
22+
23+
```sh
24+
sudo modprobe gpio-sim cuse
25+
sudo env BIN_SERVER=... BIN_GPIO_SIM=... BIN_SPI_CUSE=... SPIDEV_TEST=... \
26+
tests/e2e/drdy/e2e.sh
27+
```
28+
29+
## Run in QEMU
30+
31+
`run-qemu.sh` builds the binaries, a minimal Ubuntu root filesystem, and boots a
32+
distribution kernel (software emulation without `/dev/kvm`). Use it where the host
33+
kernel lacks the modules, for example cloud sandboxes. See the script header for
34+
host requirements.
35+
36+
Ubuntu 24.04 ships libgpiod 1.6, so the script uses the `gpiomon <chip> <line>`
37+
syntax of that version.

‎tests/e2e/drdy/e2e.sh‎

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#!/usr/bin/env bash
2+
# Real Linux ABI end-to-end test for ADR 0011:
3+
#
4+
# sensor model -> delayed DRDY -> signal router -> GPIO runtime -> gpio-sim
5+
# -> /dev/gpiochipN -> gpiomon -> SPI read (spidev_test) -> read-clear -> DRDY falls
6+
#
7+
# Needs root, the gpio-sim and cuse kernel modules, configfs, libgpiod tools and an
8+
# upstream spidev_test. Run it directly on such a host or use run-qemu.sh.
9+
set -u
10+
11+
REPO="${REPO:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)}"
12+
BIN_SERVER="${BIN_SERVER:-vds-server}"
13+
BIN_GPIO_SIM="${BIN_GPIO_SIM:-vds4e-gpio-sim}"
14+
BIN_SPI_CUSE="${BIN_SPI_CUSE:-vds4e-spi-cuse}"
15+
SPIDEV_TEST="${SPIDEV_TEST:-spidev_test}"
16+
WORK="$(mktemp -d)"
17+
export HOME="$WORK/home"
18+
mkdir -p "$HOME"
19+
SOCKET="$WORK/vds4e.sock"
20+
PIDS=()
21+
FAIL=0
22+
23+
cleanup() {
24+
for pid in "${PIDS[@]}"; do kill "$pid" 2>/dev/null; done
25+
wait 2>/dev/null
26+
rm -rf "$WORK"
27+
}
28+
trap cleanup EXIT
29+
30+
now() { date +%s.%N; }
31+
ms() { awk -v a="$1" -v b="$2" 'BEGIN { printf "%.1f", (b - a) * 1000 }'; }
32+
check() {
33+
if [ "$2" = "$3" ]; then echo "PASS $1 (got '$2')"; else echo "FAIL $1 (expected '$3', got '$2')"; FAIL=1; fi
34+
}
35+
wait_for() { # <seconds*10> <test command...>
36+
local tries=$1; shift
37+
for _ in $(seq "$tries"); do "$@" && return 0; sleep 0.1; done
38+
return 1
39+
}
40+
await_exit() { # wait up to 5 s for a background pid to exit
41+
local pid=$1
42+
for _ in $(seq 50); do kill -0 "$pid" 2>/dev/null || return 0; sleep 0.1; done
43+
kill "$pid" 2>/dev/null
44+
return 1
45+
}
46+
47+
cat > "$WORK/server.yaml" <<YAML
48+
schema_version: 1
49+
server: { control_address: '127.0.0.1:0' }
50+
data_plane: { unix_socket: $SOCKET }
51+
observability: { log_level: debug }
52+
event_store: { enabled: false }
53+
topology: $WORK/topology.yaml
54+
device_packages:
55+
- $REPO/device-models/examples/spi-sensor-drdy
56+
- $REPO/device-models/examples/generic-gpio-bank
57+
YAML
58+
cat > "$WORK/topology.yaml" <<YAML
59+
schema_version: 1
60+
connections:
61+
- from: spi-sensor-drdy.drdy
62+
to: generic-gpio-bank-32.GPIO16
63+
YAML
64+
65+
"$BIN_SERVER" --config "$WORK/server.yaml" > "$WORK/server.log" 2>&1 &
66+
PIDS+=($!)
67+
wait_for 100 test -S "$SOCKET" || { echo "FAIL server socket"; cat "$WORK/server.log"; exit 1; }
68+
69+
"$BIN_GPIO_SIM" --name vds4e-e2e-gpio --label "VDS4E E2E GPIO" --lines 32 \
70+
--device-id generic-gpio-bank-32 --socket "$SOCKET" > "$WORK/gpio.out" 2> "$WORK/gpio.err" &
71+
PIDS+=($!)
72+
wait_for 100 test -s "$WORK/gpio.out"
73+
CHIP=$(head -1 "$WORK/gpio.out")
74+
[ -c "$CHIP" ] || { echo "FAIL no gpiochip ($CHIP)"; cat "$WORK/gpio.err"; exit 1; }
75+
echo "### gpio-sim controller: $CHIP"
76+
77+
"$BIN_SPI_CUSE" --name spidev0.0 --device-id spi-sensor-drdy --socket "$SOCKET" \
78+
> "$WORK/spi.out" 2> "$WORK/spi.err" &
79+
PIDS+=($!)
80+
wait_for 100 test -c /dev/spidev0.0 || { echo "FAIL no /dev/spidev0.0"; cat "$WORK/spi.err"; exit 1; }
81+
gpiodetect
82+
sleep 0.3
83+
84+
echo; echo "=== cycle 1: conversion -> DRDY rising edge -> read-clear -> falling edge ==="
85+
check "DRDY low at idle" "$(gpioget "$CHIP" 16)" 0
86+
87+
gpiomon --num-events=1 --rising-edge --format='%e line=%o' "$CHIP" 16 > "$WORK/rise1.out" 2>&1 &
88+
RISE=$!; sleep 0.5
89+
T0=$(now)
90+
"$SPIDEV_TEST" -D /dev/spidev0.0 -p '\x10' > "$WORK/start1.out" 2>&1 || { echo "FAIL spidev_test START"; cat "$WORK/start1.out"; FAIL=1; }
91+
if await_exit "$RISE"; then
92+
echo "gpiomon: $(cat "$WORK/rise1.out") (~$(ms "$T0" "$(now)") ms incl. spidev_test start-up)"
93+
else echo "FAIL no rising edge within 5 s"; FAIL=1; fi
94+
check "DRDY high after the edge" "$(gpioget "$CHIP" 16)" 1
95+
sleep 0.4
96+
check "DRDY stays high until the sample is read" "$(gpioget "$CHIP" 16)" 1
97+
98+
gpiomon --num-events=1 --falling-edge --format='%e line=%o' "$CHIP" 16 > "$WORK/fall1.out" 2>&1 &
99+
FALL=$!; sleep 0.5
100+
"$SPIDEV_TEST" -D /dev/spidev0.0 -v -p '\x20\x00\x00\x00\x00' > "$WORK/read1.out" 2>&1 || { echo "FAIL spidev_test READ"; FAIL=1; }
101+
sed 's/^/ /' "$WORK/read1.out" | grep -E "TX|RX"
102+
if await_exit "$FALL"; then echo "gpiomon: $(cat "$WORK/fall1.out")"; else echo "FAIL no falling edge within 5 s"; FAIL=1; fi
103+
check "DRDY low after read-clear" "$(gpioget "$CHIP" 16)" 0
104+
if grep -q "RX | 00 80 00 12 34" "$WORK/read1.out"; then echo "PASS sample bytes 80 00 12 34"; else echo "FAIL sample bytes"; FAIL=1; fi
105+
106+
echo; echo "=== cycle 2: the signal re-arms ==="
107+
gpiomon --num-events=1 --rising-edge --format='%e line=%o' "$CHIP" 16 > "$WORK/rise2.out" 2>&1 &
108+
RISE=$!; sleep 0.3
109+
"$SPIDEV_TEST" -D /dev/spidev0.0 -p '\x10' > /dev/null 2>&1
110+
if await_exit "$RISE"; then echo "PASS second rising edge: $(cat "$WORK/rise2.out")"; else echo "FAIL no second edge"; FAIL=1; fi
111+
check "DRDY high in cycle 2" "$(gpioget "$CHIP" 16)" 1
112+
113+
echo; echo "=== negative: a read without a new sample must not raise DRDY ==="
114+
"$SPIDEV_TEST" -D /dev/spidev0.0 -p '\x20\x00\x00\x00\x00' > /dev/null 2>&1; sleep 0.3
115+
check "DRDY low after the read" "$(gpioget "$CHIP" 16)" 0
116+
"$SPIDEV_TEST" -D /dev/spidev0.0 -p '\x20\x00\x00\x00\x00' > /dev/null 2>&1; sleep 0.3
117+
check "DRDY still low after a second read" "$(gpioget "$CHIP" 16)" 0
118+
119+
echo; echo "### server-side timeline"
120+
grep "transaction completed" "$WORK/server.log" | grep spi-sensor \
121+
| grep -o '"timestamp":"[^"]*"\|"request":"[^"]*"' | paste - - | sed 's/^/ spi: /'
122+
grep "GPIO edge detected" "$WORK/server.log" | grep -o '"timestamp":"[^"]*"\|"edges":"[^"]*"' | paste - - | sed 's/^/ gpio: /'
123+
grep -Ei '"level":"(WARN|ERROR)"|panic' "$WORK/server.log" | head -5
124+
echo "### RESULT: $([ "$FAIL" = 0 ] && echo ALL_PASS || echo FAILURES)"
125+
exit "$FAIL"

‎tests/e2e/drdy/run-qemu.sh‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/usr/bin/env bash
2+
# Runs e2e.sh inside a QEMU guest booted from a stock distribution kernel. Use it
3+
# when the host kernel lacks gpio-sim/cuse/configfs (cloud sandboxes, containers).
4+
#
5+
# Host requirements: qemu-system-x86, debootstrap, e2fsprogs, gcc, cmake,
6+
# libfuse3-dev, a Rust toolchain, root, and an installed distribution kernel with
7+
# its module packages (Ubuntu: linux-image-generic + linux-modules-extra-<ver>).
8+
# Network access is needed once for debootstrap and the upstream spidev_test.
9+
set -euo pipefail
10+
11+
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
12+
WORK="${WORK:-$REPO/build/e2e-drdy}"
13+
KVER="${KVER:-$(ls /lib/modules | sort -V | tail -1)}"
14+
SPIDEV_URL="https://raw.githubusercontent.com/torvalds/linux/v6.8/tools/spi/spidev_test.c"
15+
mkdir -p "$WORK"
16+
17+
echo "== build host binaries"
18+
cargo build --release --locked -p vds-server --manifest-path "$REPO/Cargo.toml"
19+
TARGET_DIR="$(cargo metadata --format-version 1 --no-deps --manifest-path "$REPO/Cargo.toml" \
20+
| python3 -c 'import json,sys; print(json.load(sys.stdin)["target_directory"])')"
21+
for adapter in spi-cuse gpio-sim; do
22+
cmake -S "$REPO/adapters/$adapter" -B "$WORK/build-$adapter" -DCMAKE_BUILD_TYPE=Release > /dev/null
23+
cmake --build "$WORK/build-$adapter" --parallel > /dev/null
24+
done
25+
[ -f "$WORK/spidev_test.c" ] || curl -fsSL -o "$WORK/spidev_test.c" "$SPIDEV_URL"
26+
gcc -O2 -o "$WORK/spidev_test" "$WORK/spidev_test.c"
27+
28+
echo "== root filesystem"
29+
if [ ! -d "$WORK/rootfs" ]; then
30+
debootstrap --variant=minbase --components=main,universe \
31+
--include=gpiod,kmod,libfuse3-3,libgcc-s1,procps \
32+
noble "$WORK/rootfs" http://archive.ubuntu.com/ubuntu
33+
fi
34+
ROOT="$WORK/rootfs"
35+
rm -rf "$ROOT/lib/modules" "$ROOT/vds"
36+
mkdir -p "$ROOT/lib/modules" "$ROOT/vds/repo/device-models/examples" "$ROOT/vds/repo/tests/e2e/drdy"
37+
cp -a "/lib/modules/$KVER" "$ROOT/lib/modules/"
38+
cp "$TARGET_DIR/release/vds-server" "$WORK/build-spi-cuse/vds4e-spi-cuse" \
39+
"$WORK/build-gpio-sim/vds4e-gpio-sim" "$WORK/spidev_test" "$ROOT/usr/local/bin/"
40+
cp -a "$REPO/device-models/examples/spi-sensor-drdy" \
41+
"$REPO/device-models/examples/generic-gpio-bank" "$ROOT/vds/repo/device-models/examples/"
42+
cp "$REPO/tests/e2e/drdy/e2e.sh" "$ROOT/vds/repo/tests/e2e/drdy/"
43+
cat > "$ROOT/vds/init.sh" <<'INIT'
44+
#!/bin/bash
45+
export PATH=/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOME=/root
46+
mount -t proc proc /proc; mount -t sysfs sys /sys
47+
mountpoint -q /dev || mount -t devtmpfs dev /dev
48+
mkdir -p /dev/pts /tmp; mount -t devpts devpts /dev/pts; mount -t tmpfs tmp /tmp
49+
mount -t configfs configfs /sys/kernel/config
50+
echo "### guest kernel $(uname -r)"
51+
modprobe gpio-sim && modprobe cuse || echo "### MODULE LOAD FAILED"
52+
REPO=/vds/repo bash /vds/repo/tests/e2e/drdy/e2e.sh
53+
echo "### E2E_EXIT=$?"
54+
sync; echo o > /proc/sysrq-trigger; sleep 5
55+
INIT
56+
chmod +x "$ROOT/vds/init.sh"
57+
58+
echo "== image and boot (software emulation if /dev/kvm is missing)"
59+
rm -f "$WORK/root.img"
60+
truncate -s 1500M "$WORK/root.img"
61+
mke2fs -q -t ext4 -d "$ROOT" -L vdsroot "$WORK/root.img"
62+
ACCEL=()
63+
[ -w /dev/kvm ] && ACCEL=(-enable-kvm -cpu host) || ACCEL=(-cpu max)
64+
timeout 900 qemu-system-x86_64 -m 2G -smp 4 "${ACCEL[@]}" -nographic -no-reboot \
65+
-kernel "/boot/vmlinuz-$KVER" \
66+
-append "root=/dev/vda rw console=ttyS0 panic=-1 init=/vds/init.sh quiet loglevel=3" \
67+
-drive "file=$WORK/root.img,format=raw,if=virtio" | tee "$WORK/guest.log"
68+
grep -q "### RESULT: ALL_PASS" "$WORK/guest.log"

0 commit comments

Comments
 (0)