* CI: run exclusively GUI tests in the GUI test steps * Fix linter error Fix the unsupported operand type(s) for | (unsupported-binary-operation) linter error. It appears the linter uses or assumes Python <3.10. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * CI: move __future__ import to the top of the module... ... to fix linter error. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
169 lines
5.3 KiB
Python
169 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
|
"""
|
|
run_gui_tests.py
|
|
|
|
List registered tests via `FreeCAD -t`, filter for GUI tests (names containing 'Gui'), and run each
|
|
GUI test module using the specified FreeCAD executable.
|
|
|
|
Usage:
|
|
run_gui_tests.py [FREECAD_EXEC]
|
|
|
|
If FREECAD_EXEC is omitted the script falls back to 'FreeCAD' on PATH.
|
|
If FREECAD_EXEC is a directory containing bin/FreeCAD, that binary is used.
|
|
If FREECAD_EXEC is an executable path, it is used directly.
|
|
|
|
This script returns 0 if all GUI modules run successfully. Otherwise it returns the last non-zero
|
|
exit code.
|
|
"""
|
|
from __future__ import annotations
|
|
import sys
|
|
import subprocess
|
|
import os
|
|
from pathlib import Path
|
|
|
|
|
|
def find_executable(arg: str | None) -> str:
|
|
"""Return the FreeCAD executable path to use.
|
|
|
|
If `arg` is None or empty, returns the plain name 'FreeCAD' which will be looked up on PATH. If
|
|
`arg` is a directory and contains `bin/FreeCAD` that binary will be returned. If `arg` is a file
|
|
path it is returned as-is. Otherwise the original argument is returned.
|
|
|
|
Common use cases: use the FreeCAD binary from a build directory or an installed FreeCAD prefix.
|
|
"""
|
|
if not arg:
|
|
return "FreeCAD"
|
|
p = Path(arg)
|
|
if p.is_dir():
|
|
candidate = p / "bin" / "FreeCAD"
|
|
if candidate.exists():
|
|
return str(candidate)
|
|
if p.is_file():
|
|
return str(p)
|
|
# fallback: return as-is (may be on PATH)
|
|
return arg
|
|
|
|
|
|
def validate_executable(path: str) -> tuple[bool, str]:
|
|
"""Return (ok, message). Checks if the executable exists or is likely on PATH.
|
|
|
|
This is best effort: if a bare name is given (e.g. 'FreeCAD') we can't stat it here, so we
|
|
accept it but warn. If the path points to a file, we check executability. Also warn if the name
|
|
looks like the CLI-only 'FreeCADCmd'.
|
|
"""
|
|
p = Path(path)
|
|
if p.is_file():
|
|
if not os.access(str(p), os.X_OK):
|
|
return False, f"File exists but is not executable: {path}"
|
|
if p.name.endswith("FreeCADCmd"):
|
|
return (
|
|
True,
|
|
(
|
|
"Warning: executable looks like 'FreeCADCmd' (CLI); GUI tests require the GUI "
|
|
"binary 'FreeCAD'."
|
|
),
|
|
)
|
|
return True, ""
|
|
# Bare name or non-existent path: accept but warn
|
|
if p.name.endswith("FreeCADCmd"):
|
|
return (
|
|
True,
|
|
(
|
|
"Warning: executable name looks like 'FreeCADCmd' (CLI); GUI tests require the GUI "
|
|
"binary 'FreeCAD'."
|
|
),
|
|
)
|
|
return True, ""
|
|
|
|
|
|
def run_and_capture(cmd: list[str]) -> tuple[int, str]:
|
|
"""Run `cmd` and return (returncode, combined stdout+stderr string).
|
|
|
|
If the executable is not found a return code of 127 is returned and a small error string is
|
|
provided as output to aid diagnosis.
|
|
"""
|
|
try:
|
|
proc = subprocess.run(
|
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False
|
|
)
|
|
return proc.returncode, proc.stdout
|
|
except FileNotFoundError:
|
|
return 127, f"Executable not found: {cmd[0]}\n"
|
|
|
|
|
|
def parse_registered_tests(output: str) -> list[str]:
|
|
"""Parse output from `FreeCAD -t` and return a list of registered test unit names.
|
|
|
|
The function looks for the section starting with the literal 'Registered test units:' and
|
|
then collects non-empty, stripped lines from that point onwards as test names.
|
|
"""
|
|
lines = output.splitlines()
|
|
tests: list[str] = []
|
|
started = False
|
|
for ln in lines:
|
|
if not started:
|
|
if "Registered test units:" in ln:
|
|
started = True
|
|
continue
|
|
s = ln.strip()
|
|
if not s:
|
|
# allow blank lines but keep going
|
|
continue
|
|
tests.append(s)
|
|
return tests
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
"""Entry point: run GUI test modules registered in the FreeCAD executable.
|
|
|
|
Returns the last non-zero exit code from any GUI test module, or 0 on success.
|
|
"""
|
|
exec_arg = argv[1] if len(argv) > 1 else None
|
|
freecad_exec = find_executable(exec_arg)
|
|
|
|
print(f"Using FreeCAD executable: {freecad_exec}")
|
|
|
|
ok, msg = validate_executable(freecad_exec)
|
|
if msg:
|
|
print(msg, file=sys.stderr)
|
|
if not ok:
|
|
print(f"Aborting: invalid FreeCAD executable: {freecad_exec}", file=sys.stderr)
|
|
return 3
|
|
|
|
code, out = run_and_capture([freecad_exec, "-t"])
|
|
if code != 0:
|
|
print(
|
|
f"Warning: listing tests returned exit code {code}; attempting to parse output anyway",
|
|
file=sys.stderr,
|
|
)
|
|
|
|
tests = parse_registered_tests(out)
|
|
if not tests:
|
|
print("No registered tests found; exiting with success.")
|
|
return 0
|
|
|
|
gui_tests = [t for t in tests if "Gui" in t]
|
|
if not gui_tests:
|
|
print("No GUI tests found in registered tests; nothing to run.")
|
|
return 0
|
|
|
|
print("Found GUI test modules:")
|
|
for t in gui_tests:
|
|
print(" ", t)
|
|
|
|
last_rc = 0
|
|
for mod in gui_tests:
|
|
print(f"\nRunning GUI tests for module: {mod}")
|
|
rc, out = run_and_capture([freecad_exec, "-t", mod])
|
|
print(out)
|
|
if rc != 0:
|
|
print(f"Module {mod} exited with code {rc}", file=sys.stderr)
|
|
last_rc = rc
|
|
|
|
return last_rc
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|