vibefpmgui.test_runner

Runner wrapper passed to fpm test --runner.

fpm calls <runner> <test executable> [args...] for each test, one after another. This script prints a marker before and after running the test so the GUI can tell where each test starts and ends and what it returned::

@@vibefpm:start:check_fast@@
...test output...
@@vibefpm:end:check_fast:0:0.012@@

With --collect first, the test isn't run: its command line is printed instead (@@vibefpm:exe:check_fast@@["/abs/path/check_fast", "arg"]), for the GUI to run the tests itself, in parallel (see vibefpmgui.parallel_tests).

A runner the user gave fpm themselves (e.g. mpirun -np 4) is passed in the VIBEFPM_INNER_RUNNER environment variable and still wraps the test.

Only the standard library is used, so this runs with python -I.

 1"""Runner wrapper passed to ``fpm test --runner``.
 2
 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::
 6
 7    @@vibefpm:start:check_fast@@
 8    ...test output...
 9    @@vibefpm:end:check_fast:0:0.012@@
10
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`).
15
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.
18
19Only the standard library is used, so this runs with ``python -I``.
20"""
21
22import json
23import os
24import shlex
25import subprocess
26import sys
27import time
28
29MARKER = "@@vibefpm:{}@@"
30
31
32def executable_name(executable: str) -> str:
33    name = os.path.basename(executable)
34    return name[:-4] if name.lower().endswith(".exe") else name
35
36
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
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)
43
44
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:
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
57
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
71
72
73if __name__ == "__main__":
74    sys.exit(main(sys.argv[1:]))
MARKER = '@@vibefpm:{}@@'
def executable_name(executable: str) -> str:
33def executable_name(executable: str) -> str:
34    name = os.path.basename(executable)
35    return name[:-4] if name.lower().endswith(".exe") else name
def split_runner(runner: str) -> list[str]:
38def split_runner(runner: str) -> list[str]:
39    """A runner command (e.g. "mpirun -np 4") as a list."""
40    if os.name == "nt":  # backslashes are path separators there, not escapes
41        parts = shlex.split(runner, posix=False)
42        return [p[1:-1] if len(p) >= 2 and p[0] == p[-1] and p[0] in "\"'" else p for p in parts]
43    return shlex.split(runner)

A runner command (e.g. "mpirun -np 4") as a list.

def main(argv: list[str]) -> int:
46def main(argv: list[str]) -> int:
47    if not argv:
48        print("usage: test_runner.py EXECUTABLE [ARGS...]", file=sys.stderr)
49        return 2
50    if argv[0] == "--collect":
51        if len(argv) < 2:
52            return 2
53        name = executable_name(argv[1])
54        print(MARKER.format(f"exe:{name}") + json.dumps([os.path.abspath(argv[1]), *argv[2:]]), flush=True)
55        return 0
56    name = executable_name(argv[0])
57    command = split_runner(os.environ.get("VIBEFPM_INNER_RUNNER", "")) + argv
58
59    # The test writes straight to our stdout, so flush our own text first.
60    # The leading newline keeps the marker off a line the previous test left
61    # unfinished.
62    print("\n" + MARKER.format(f"start:{name}"), flush=True)
63    started = time.monotonic()
64    try:
65        code = subprocess.call(command)
66    except OSError as exc:
67        print(f"could not run {command[0]}: {exc}", flush=True)
68        code = 127
69    seconds = time.monotonic() - started
70    print("\n" + MARKER.format(f"end:{name}:{code}:{seconds:.3f}"), flush=True)
71    return code