Coverage for vibefpmgui/test_runner.py: 86%
39 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-01 20:51 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-01 20:51 +0000
1"""Runner wrapper passed to ``fpm test --runner``.
3fpm calls ``<runner> <test executable> [args...]`` for each test, one after
4another. This script prints a marker before and after running the test so
5the GUI can tell where each test starts and ends and what it returned::
7 @@vibefpm:start:check_fast@@
8 ...test output...
9 @@vibefpm:end:check_fast:0:0.012@@
11With ``--collect`` first, the test isn't run: its command line is printed
12instead (``@@vibefpm:exe:check_fast@@["/abs/path/check_fast", "arg"]``), for
13the GUI to run the tests itself, in parallel (see
14`vibefpmgui.parallel_tests`).
16A runner the user gave fpm themselves (e.g. ``mpirun -np 4``) is passed in
17the ``VIBEFPM_INNER_RUNNER`` environment variable and still wraps the test.
19Only the standard library is used, so this runs with ``python -I``.
20"""
22import json
23import os
24import shlex
25import subprocess
26import sys
27import time
29MARKER = "@@vibefpm:{}@@"
32def executable_name(executable: str) -> str:
33 name = os.path.basename(executable)
34 return name[:-4] if name.lower().endswith(".exe") else name
37def split_runner(runner: str) -> list[str]:
38 """A runner command (e.g. "mpirun -np 4") as a list."""
39 if os.name == "nt": # backslashes are path separators there, not escapes 39 ↛ 40line 39 didn't jump to line 40 because the condition on line 39 was never true
40 parts = shlex.split(runner, posix=False)
41 return [p[1:-1] if len(p) >= 2 and p[0] == p[-1] and p[0] in "\"'" else p for p in parts]
42 return shlex.split(runner)
45def main(argv: list[str]) -> int:
46 if not argv:
47 print("usage: test_runner.py EXECUTABLE [ARGS...]", file=sys.stderr)
48 return 2
49 if argv[0] == "--collect":
50 if len(argv) < 2: 50 ↛ 51line 50 didn't jump to line 51 because the condition on line 50 was never true
51 return 2
52 name = executable_name(argv[1])
53 print(MARKER.format(f"exe:{name}") + json.dumps([os.path.abspath(argv[1]), *argv[2:]]), flush=True)
54 return 0
55 name = executable_name(argv[0])
56 command = split_runner(os.environ.get("VIBEFPM_INNER_RUNNER", "")) + argv
58 # The test writes straight to our stdout, so flush our own text first.
59 # The leading newline keeps the marker off a line the previous test left
60 # unfinished.
61 print("\n" + MARKER.format(f"start:{name}"), flush=True)
62 started = time.monotonic()
63 try:
64 code = subprocess.call(command)
65 except OSError as exc:
66 print(f"could not run {command[0]}: {exc}", flush=True)
67 code = 127
68 seconds = time.monotonic() - started
69 print("\n" + MARKER.format(f"end:{name}:{code}:{seconds:.3f}"), flush=True)
70 return code
73if __name__ == "__main__": 73 ↛ 74line 73 didn't jump to line 74 because the condition on line 73 was never true
74 sys.exit(main(sys.argv[1:]))