CI: run exclusively GUI tests in the GUI test steps (#23933)
* 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>
This commit is contained in:
168
.github/scripts/run_gui_tests.py
vendored
Normal file
168
.github/scripts/run_gui_tests.py
vendored
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
#!/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))
|
||||||
6
.github/workflows/sub_buildUbuntu.yml
vendored
6
.github/workflows/sub_buildUbuntu.yml
vendored
@@ -173,7 +173,8 @@ jobs:
|
|||||||
uses: ./.github/workflows/actions/runPythonTests
|
uses: ./.github/workflows/actions/runPythonTests
|
||||||
with:
|
with:
|
||||||
testDescription: "GUI tests on build dir"
|
testDescription: "GUI tests on build dir"
|
||||||
testCommand: xvfb-run ${{ env.builddir }}/bin/FreeCAD -t 0
|
# Use Python wrapper to run only GUI-registered tests with the built FreeCAD executable
|
||||||
|
testCommand: xvfb-run python3 ./.github/scripts/run_gui_tests.py "${{ env.builddir }}"
|
||||||
logFile: ${{ env.logdir }}TestGUIBuild.log
|
logFile: ${{ env.logdir }}TestGUIBuild.log
|
||||||
reportFile: ${{env.reportdir}}${{ env.reportfilename }}
|
reportFile: ${{env.reportdir}}${{ env.reportfilename }}
|
||||||
|
|
||||||
@@ -207,7 +208,8 @@ jobs:
|
|||||||
uses: ./.github/workflows/actions/runPythonTests
|
uses: ./.github/workflows/actions/runPythonTests
|
||||||
with:
|
with:
|
||||||
testDescription: "GUI tests on install"
|
testDescription: "GUI tests on install"
|
||||||
testCommand: xvfb-run FreeCAD -t 0
|
# Use Python wrapper to run only GUI-registered tests using the installed FreeCAD
|
||||||
|
testCommand: xvfb-run python3 ./.github/scripts/run_gui_tests.py "FreeCAD"
|
||||||
logFile: ${{ env.logdir }}TestGUIInstall.log
|
logFile: ${{ env.logdir }}TestGUIInstall.log
|
||||||
reportFile: ${{env.reportdir}}${{ env.reportfilename }}
|
reportFile: ${{env.reportdir}}${{ env.reportfilename }}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user