mirror of
https://github.com/raspberrypi/pico-sdk.git
synced 2026-03-14 21:19:43 +01:00
Some checks are pending
Bazel presubmit checks / bazel-build-check (macos-latest) (push) Waiting to run
Bazel presubmit checks / bazel-build-check (ubuntu-latest) (push) Waiting to run
Bazel presubmit checks / other-bazel-checks (push) Waiting to run
Check Configs / check-configs (push) Waiting to run
CMake / build (push) Waiting to run
Build on macOS / build (push) Waiting to run
Build on Windows / build (push) Waiting to run
add a test driver script to run a matrix of tests via gdb on device
258 lines
No EOL
11 KiB
Python
Executable file
258 lines
No EOL
11 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""
|
|
Script to run a matrix of tests via gdb via a running OpenOCD capturing UART output and checking for "PASSED" -
|
|
execution stops with a timeout or when GDB halts (e.g. breakpoint)
|
|
|
|
It requires 'grabserial'.
|
|
|
|
Iterates over path_segments and elf_filenames.
|
|
|
|
For every combination where <path_prefix>/<path_segment>/<path_postfix>/<elf_filename> exists:
|
|
- Flashes it via OpenOCD (program + verify + reset + exit)
|
|
- Captures UART output using grabserial
|
|
- Saves to <output_dir>/<sanitized_path_segment>.<elf_filename>.out
|
|
|
|
At the end:
|
|
- Checks that the last line of every generated .out file is exactly "PASSED" (after strip)
|
|
- Prints success count / attempted count
|
|
- Exits with 0 only if ALL attempted tests succeeded, else 1
|
|
|
|
Example usage:
|
|
|
|
$ tools/run_test_matrix.py \
|
|
--path-segments \
|
|
gcc_build \
|
|
clang_build \
|
|
--elf-filenames \
|
|
custom_float_funcs_test_compiler.elf \
|
|
custom_float_funcs_test_pico_dcp.elf \
|
|
custom_float_funcs_test_pico.elf \
|
|
custom_float_funcs_test_pico_vfp.elf \
|
|
--path-postfix \
|
|
test/pico_float_test \
|
|
-- \
|
|
out_dir
|
|
|
|
Which will run each of the following if they exist:
|
|
gcc_build/test/pico_float_test/custom_float_funcs_test_compiler.elf
|
|
gcc_build/test/pico_float_test/custom_float_funcs_test_pico_dcp.elf
|
|
gcc_build/test/pico_float_test/custom_float_funcs_test_pico.elf
|
|
gcc_build/test/pico_float_test/custom_float_funcs_test_pico_vfp.elf
|
|
clang_build/test/pico_float_test/custom_float_funcs_test_compiler.elf
|
|
clang_build/test/pico_float_test/custom_float_funcs_test_pico_dcp.elf
|
|
clang_build/test/pico_float_test/custom_float_funcs_test_pico.elf
|
|
clang_build/test/pico_float_test/custom_float_funcs_test_pico_vfp.elf
|
|
storing their UART output in:
|
|
out_dir/gcc_build.custom_float_funcs_test_compiler.elf.out
|
|
out_dir/gcc_build.custom_float_funcs_test_pico_dcp.elf.out
|
|
out_dir/gcc_build.custom_float_funcs_test_pico.elf.out
|
|
out_dir/gcc_build.custom_float_funcs_test_pico_vfp.elf.out
|
|
out_dir/clang_build.custom_float_funcs_test_compiler.elf.out
|
|
out_dir/clang_build.custom_float_funcs_test_pico_dcp.elf.out
|
|
out_dir/clang_build.custom_float_funcs_test_pico.elf.out
|
|
out_dir/clang_build.custom_float_funcs_test_pico_vfp.elf.out
|
|
|
|
This would run the same tests:
|
|
(but store the output in e.g out_dir/gcc_build.pico_float_test.custom_float_funcs_test_compiler.elf.out )
|
|
|
|
$ tools/run_test_matrix.py \
|
|
--path-segments \
|
|
gcc_build \
|
|
clang_build \
|
|
--elf-filenames \
|
|
pico_float_test/custom_float_funcs_test_compiler.elf \
|
|
pico_float_test/custom_float_funcs_test_pico_dcp.elf \
|
|
pico_float_test/custom_float_funcs_test_pico.elf \
|
|
pico_float_test/custom_float_funcs_test_pico_vfp.elf \
|
|
--path-postfix \
|
|
test \
|
|
-- \
|
|
out_dir
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import signal
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="RP2xxx ELF test runner with OpenOCD + grabserial UART capture")
|
|
parser.add_argument("output_dir", help="Directory where *.out files will be written (will be created if missing)")
|
|
parser.add_argument("--path-segments", nargs="+", required=True,
|
|
help="Space-separated list of path segments; e.g. foo, bar, foo/bar/humbug")
|
|
parser.add_argument("--path-prefix", default=".",
|
|
help="Path prefix")
|
|
parser.add_argument("--path-postfix", default="",
|
|
help="Path postfix")
|
|
parser.add_argument("--gdb", default="arm-none-eabi-gdb",
|
|
help="gdb executable")
|
|
parser.add_argument("--gdb-timeout", type=int, default=60,
|
|
help="gdb timeout for executable if it doesn't finish")
|
|
parser.add_argument("--openocd-server", default="localhost:3333",
|
|
help="openocd server address:port"),
|
|
parser.add_argument("--elf-filenames", nargs="+", required=True,
|
|
help="Space-separated list of ELF filenames (including .elf), e.g. test1.elf test2.elf")
|
|
parser.add_argument("--serial-port", default="/dev/ttyACM0", help="UART device (default: /dev/ttyACM0)")
|
|
parser.add_argument("--baud", type=int, default=115200, help="UART baud rate (default: 115200)")
|
|
|
|
args = parser.parse_args()
|
|
|
|
os.makedirs(args.output_dir, exist_ok=True)
|
|
|
|
attempted = 0
|
|
succeeded = 0
|
|
|
|
for seg in args.path_segments:
|
|
for elf in args.elf_filenames:
|
|
full_path = os.path.join(args.path_prefix, seg, args.path_postfix, elf)
|
|
if not os.path.isfile(full_path):
|
|
print(f"\n=== Skipping missing ELF {full_path} ===")
|
|
continue
|
|
|
|
attempted += 1
|
|
sanitized_out_file = f"{seg}.{elf}.out".replace("/",".")
|
|
outfile = os.path.join(args.output_dir, sanitized_out_file)
|
|
|
|
print(f"\n=== Processing {seg}/{elf} ===")
|
|
print(f" ELF: {full_path}")
|
|
print(f" Out: {outfile}")
|
|
|
|
# ====================== START GRABSERIAL EARLY FOR FULL UART CAPTURE ======================
|
|
print(f" Starting grabserial capture early on {args.serial_port} @ {args.baud} baud...")
|
|
|
|
logger_proc = None
|
|
try:
|
|
logger_proc = subprocess.Popen(
|
|
[
|
|
"grabserial",
|
|
"-d", args.serial_port,
|
|
"-b", str(args.baud),
|
|
"-o", outfile, # output file
|
|
"--quiet", # suppress extra messages
|
|
],
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.PIPE,
|
|
preexec_fn=os.setsid,
|
|
)
|
|
print(f" grabserial started (PID {logger_proc.pid}) — capturing from now on")
|
|
|
|
# Give grabserial a moment to open the port and settle
|
|
time.sleep(0.5)
|
|
|
|
# ====================== FLASH + START VIA GDB ======================
|
|
print(" Flashing and running via GDB (breakpoint signals test end)...")
|
|
|
|
gdb_cmd = [
|
|
args.gdb, "--batch", "--nx", full_path,
|
|
"-ex", f"target extended-remote {args.openocd_server}",
|
|
"-ex", "monitor reset init",
|
|
"-ex", "load",
|
|
"-ex", "monitor verify", # optional
|
|
"-ex", "continue",
|
|
]
|
|
|
|
gdb_result = subprocess.run(
|
|
gdb_cmd,
|
|
check=False,
|
|
timeout=args.gdb_timeout,
|
|
capture_output=True,
|
|
text=True, encoding="utf-8", errors="replace"
|
|
)
|
|
|
|
# Show GDB output always (super useful for diagnosing)
|
|
combined_output = gdb_result.stdout + "\n" + gdb_result.stderr
|
|
if combined_output.strip():
|
|
print(" GDB output:")
|
|
print(" ────────────────────────────────────────────────")
|
|
print(combined_output.strip())
|
|
print(" ────────────────────────────────────────────────")
|
|
|
|
if "breakpoint" in combined_output:
|
|
print(" ✓ Breakpoint hit → test finished normally")
|
|
else:
|
|
print(" Note: No breakpoint detected in GDB output")
|
|
|
|
# Optional: tiny extra wait for any trailing UART bytes after breakpoint
|
|
time.sleep(0.5)
|
|
|
|
except FileNotFoundError:
|
|
print(" ERROR: grabserial or gdb not found in PATH")
|
|
sys.exit(1)
|
|
except subprocess.TimeoutExpired:
|
|
print(" GDB timed out")
|
|
# fall through to kill grabserial
|
|
finally:
|
|
# ====================== CLEANLY STOP GRABSERIAL ======================
|
|
if logger_proc is not None:
|
|
try:
|
|
print(" Stopping grabserial...")
|
|
os.killpg(os.getpgid(logger_proc.pid), signal.SIGTERM)
|
|
logger_proc.wait(timeout=5)
|
|
print(" grabserial stopped cleanly")
|
|
except ProcessLookupError:
|
|
print(" grabserial already exited")
|
|
except Exception as e:
|
|
print(f" Error stopping grabserial: {e}")
|
|
# forceful kill if needed
|
|
try:
|
|
os.killpg(os.getpgid(logger_proc.pid), signal.SIGKILL)
|
|
except:
|
|
pass
|
|
|
|
# ====================== CHECK WE CAPTURED SOME OUTPUT ======================
|
|
if not os.path.isfile(outfile) or os.path.getsize(outfile) == 0:
|
|
print(" No output captured")
|
|
continue
|
|
|
|
# ====================== PRINT THE CAPTURED OUTPUT TO CONSOLE ======================
|
|
print("\n" + "="*60)
|
|
print(f"Full captured UART output from {seg}/{elf}:")
|
|
print("="*60)
|
|
|
|
try:
|
|
with open(outfile, "r", encoding="utf-8", errors="replace") as f:
|
|
captured_content = f.read()
|
|
|
|
if captured_content.strip():
|
|
print(captured_content.rstrip()) # rstrip to avoid trailing newlines
|
|
else:
|
|
print("(No output captured)")
|
|
|
|
except FileNotFoundError:
|
|
print("Output file not found — capture may have failed")
|
|
except Exception as e:
|
|
print(f"Error reading output file: {e}")
|
|
|
|
print("="*60 + "\n")
|
|
# ====================== CHECK LAST LINE ======================
|
|
try:
|
|
with open(outfile, "r", encoding="utf-8", errors="ignore") as f:
|
|
lines = f.readlines()
|
|
last_line = lines[-1].strip() if lines else ""
|
|
if last_line == "PASSED":
|
|
succeeded += 1
|
|
print(" ✓ PASSED")
|
|
else:
|
|
print(f" ✗ FAILED (last line: '{last_line}')")
|
|
except Exception as e:
|
|
print(f" Failed to read output file: {e}")
|
|
|
|
# ====================== FINAL SUMMARY ======================
|
|
print("\n" + "="*60)
|
|
print(f"TEST SUMMARY: {succeeded}/{attempted} succeeded")
|
|
print("="*60)
|
|
|
|
if attempted == 0:
|
|
print("No ELFs were found to test.")
|
|
elif succeeded == attempted:
|
|
print("All tests passed!")
|
|
else:
|
|
print("Some tests failed.")
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |