From 8c1bfd7e4d6728920f6d06ec79967d8618c7a063 Mon Sep 17 00:00:00 2001 From: Samuel Nicholas Date: Fri, 6 Feb 2026 12:59:52 +1030 Subject: [PATCH 1/7] Add the run-tests.py script which is a replication of the bash script but can be modified for cross platform later. --- test/run-tests.py | 253 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 test/run-tests.py diff --git a/test/run-tests.py b/test/run-tests.py new file mode 100644 index 000000000..f68541b50 --- /dev/null +++ b/test/run-tests.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 +""" +run-tests.py - Robust test runner for godot-cpp test project (with temp portable Godot copy) + +Usage: + python run-tests.py [--unit-only | --TODO-only] # default: full +""" + +import argparse +import os +import re +import shutil +import signal +import subprocess +import sys +import tempfile +import time +from pathlib import Path + +# ────────────────────────────────────────────── +# Configuration +# ────────────────────────────────────────────── + +ORIGINAL_GODOT = os.environ.get("GODOT", "godot") +PROJECT_DIR = Path("project").resolve() +GODOT_PROJECT = PROJECT_DIR + +END_MARKER = "==== TESTS FINISHED ====" +PASSED_MARKER = "******** PASSED ********" +FAILED_MARKER = "******** FAILED ********" + +TIMEOUT_SEC = 180 +IMPORT_TIMEOUT_SEC = 30 + +FILTER_PATTERNS = [ + re.compile(r"Narrowing conversion"), + re.compile(r"\[\s*\d+%\s*\]"), + re.compile(r"first_scan_filesystem"), + re.compile(r"loading_editor_layout"), +] + +TEMP_EXE_NAME = "godot-temp-portable.exe" +TEMP_MARKER_NAME = "_sc_" + +# ────────────────────────────────────────────── +# Portable Temp Copy Helpers +# ────────────────────────────────────────────── + +def setup_temp_portable_godot() -> str: + """Copy Godot exe + create marker for single-session portable mode.""" + original_path = Path(ORIGINAL_GODOT).resolve() + if not original_path.is_file(): + print(f"Warning: Original Godot not found at '{original_path}' — using as-is.") + return ORIGINAL_GODOT + + temp_exe = Path.cwd() / TEMP_EXE_NAME + temp_marker = Path.cwd() / TEMP_MARKER_NAME + + try: + print(f"Creating temporary portable copy: {temp_exe}") + shutil.copy2(original_path, temp_exe) + + print(f"Creating portable marker: {temp_marker}") + temp_marker.touch(exist_ok=True) + + # Return path to temp exe + return str(temp_exe.absolute()) + + except Exception as e: + print(f"Failed to setup temp portable Godot: {e}") + print("Falling back to original executable (may pollute system data dirs)") + return ORIGINAL_GODOT + + +def cleanup_temp_portable(): + """Remove temp exe, marker, and any editor_data folder.""" + temp_exe = Path.cwd() / TEMP_EXE_NAME + temp_marker = Path.cwd() / TEMP_MARKER_NAME + editor_data = Path.cwd() / "editor_data" + + for path in [temp_exe, temp_marker]: + if path.exists(): + try: + path.unlink() + print(f"Cleaned: {path}") + except Exception as e: + print(f"Warning: Could not delete {path}: {e}") + + if editor_data.exists(): + try: + shutil.rmtree(editor_data) + print(f"Cleaned editor_data folder: {editor_data}") + except Exception as e: + print(f"Warning: Could not delete editor_data: {e}") + + +# ────────────────────────────────────────────── +# Other Helpers (unchanged from previous) +# ────────────────────────────────────────────── + +def filter_output(lines: list[str]) -> list[str]: + result = [] + for line in lines: + cleaned = line.rstrip() + if not cleaned: + continue + if any(pat.search(cleaned) for pat in FILTER_PATTERNS): + continue + result.append(cleaned) + return result + + +def is_successful(output: str) -> bool: + has_end = END_MARKER in output + has_passed = PASSED_MARKER in output + has_failed = FAILED_MARKER in output + return has_end and has_passed and not has_failed + + +def cleanup_godot_cache(): + cache_dir = PROJECT_DIR / ".godot" + if cache_dir.exists(): + print(f"Cleaning project cache: {cache_dir}") + try: + shutil.rmtree(cache_dir, ignore_errors=True) + except Exception as e: + print(f"Warning: Failed to clean .godot: {e}") + + +def run_godot(args: list[str], desc: str, godot_bin: str, timeout_sec: int = TIMEOUT_SEC) -> tuple[int, str, bool]: + print(f"\n{'─' * 10} {desc} {'─' * 10}") + print(f"→ {godot_bin} {' '.join(args)}") + + with tempfile.TemporaryDirectory() as tmpdir: + stdout_path = Path(tmpdir) / "stdout.txt" + stderr_path = Path(tmpdir) / "stderr.txt" + + cmd = [godot_bin] + args + + try: + start = time.time() + proc = subprocess.Popen( + cmd, + stdout=stdout_path.open("wb"), + stderr=stderr_path.open("wb"), + cwd=os.getcwd(), + start_new_session=True, + ) + + while proc.poll() is None: + if time.time() - start > timeout_sec: + print(f"→ TIMEOUT after {timeout_sec}s – killing") + proc.send_signal(signal.SIGTERM) + time.sleep(1) + if proc.poll() is None: + proc.kill() + proc.wait() + return 124, "TIMEOUT", True + time.sleep(0.5) + + exit_code = proc.returncode + stdout = stdout_path.read_text("utf-8", errors="replace") + stderr = stderr_path.read_text("utf-8", errors="replace") + full_output = stdout + stderr + + print(full_output.rstrip()) + print(f"→ Exit code: {exit_code}") + return exit_code, full_output, False + + except Exception as exc: + msg = f"Failed to run Godot: {exc}" + print(msg) + return 1, msg, False + + +def pre_import_project(godot_bin: str): + print("\nPre-importing project (headless, short timeout)...") + cleanup_godot_cache() + + args = ["--path", str(GODOT_PROJECT), "--import", "--headless"] + exit_code, output, timed_out = run_godot(args, "Pre-import", godot_bin, timeout_sec=IMPORT_TIMEOUT_SEC) + + if timed_out or exit_code != 0: + print("→ Pre-import failed/crashed — This is expected, continuing anyway") + else: + print("→ Pre-import completed") + +def run_tests(mode: str, godot_bin: str) -> bool: + overall_success = True + + # ── One-time preparation ── + print("\nPreparing project (one-time cleanup + pre-import)...") + cleanup_godot_cache() # Clean once at start + + pre_import_project(godot_bin) # Attempt import once (ignore failures) + + # No more cleanups after this point — let the cache persist + + if mode in ("unit", "full"): + args = ["--path", str(GODOT_PROJECT), "--debug", "--headless", "--quit"] + _, output, timed_out = run_godot(args, "Unit / headless tests", godot_bin) + + filtered = filter_output(output.splitlines()) + print("\nFiltered output:") + print("\n".join(filtered)) + + if timed_out: + print("→ Unit phase: TIMEOUT") + overall_success = False + elif not is_successful(output): + print("→ Unit phase: did NOT detect clean success") + overall_success = False + else: + print("→ Unit phase: detected PASSED") + + return overall_success + + +# ────────────────────────────────────────────── +# Main +# ────────────────────────────────────────────── + +def main(): + parser = argparse.ArgumentParser(description="Run godot-cpp test suite (temp portable Godot)") + parser.add_argument("--unit-only", action="store_const", const="unit", dest="mode") + args = parser.parse_args() + + mode = args.mode or "full" + + print(f"Original Godot: {ORIGINAL_GODOT}") + print(f"Project: {GODOT_PROJECT}") + print(f"Mode: {mode}\n") + + # Setup temp portable copy + godot_bin = setup_temp_portable_godot() + + try: + all_passed = run_tests(mode, godot_bin) + finally: + # Always cleanup temp files + cleanup_temp_portable() + + print("\n" + "═" * 40) + if all_passed: + print("TEST SUITE PASSED") + sys.exit(0) + else: + print("TEST SUITE FAILED") + sys.exit(1) + + +if __name__ == "__main__": + main() From df42242172c168510ff841ac6d2aed8990964f22 Mon Sep 17 00:00:00 2001 From: Samuel Nicholas Date: Fri, 6 Feb 2026 15:35:00 +1030 Subject: [PATCH 2/7] Added a verbose flag, tuning to come as a follow-up. --- test/run-tests.py | 61 +++++++++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/test/run-tests.py b/test/run-tests.py index f68541b50..b1404c43a 100644 --- a/test/run-tests.py +++ b/test/run-tests.py @@ -34,7 +34,7 @@ FILTER_PATTERNS = [ re.compile(r"Narrowing conversion"), - re.compile(r"\[\s*\d+%\s*\]"), + re.compile(r"\[\s*\d+%\s*]"), re.compile(r"first_scan_filesystem"), re.compile(r"loading_editor_layout"), ] @@ -98,7 +98,10 @@ def cleanup_temp_portable(): # Other Helpers (unchanged from previous) # ────────────────────────────────────────────── -def filter_output(lines: list[str]) -> list[str]: +def filter_output(lines: list[str], verbose: bool) -> list[str]: + if verbose: + return [line.rstrip() for line in lines if line.strip()] # just trim, keep everything + # original quiet filtering result = [] for line in lines: cleaned = line.rstrip() @@ -109,7 +112,6 @@ def filter_output(lines: list[str]) -> list[str]: result.append(cleaned) return result - def is_successful(output: str) -> bool: has_end = END_MARKER in output has_passed = PASSED_MARKER in output @@ -127,7 +129,7 @@ def cleanup_godot_cache(): print(f"Warning: Failed to clean .godot: {e}") -def run_godot(args: list[str], desc: str, godot_bin: str, timeout_sec: int = TIMEOUT_SEC) -> tuple[int, str, bool]: +def run_godot(args: list[str], desc: str, godot_bin: str, timeout_sec: int = TIMEOUT_SEC, verbose: bool = False) -> tuple[int, str, bool]: print(f"\n{'─' * 10} {desc} {'─' * 10}") print(f"→ {godot_bin} {' '.join(args)}") @@ -136,7 +138,6 @@ def run_godot(args: list[str], desc: str, godot_bin: str, timeout_sec: int = TIM stderr_path = Path(tmpdir) / "stderr.txt" cmd = [godot_bin] + args - try: start = time.time() proc = subprocess.Popen( @@ -146,7 +147,6 @@ def run_godot(args: list[str], desc: str, godot_bin: str, timeout_sec: int = TIM cwd=os.getcwd(), start_new_session=True, ) - while proc.poll() is None: if time.time() - start > timeout_sec: print(f"→ TIMEOUT after {timeout_sec}s – killing") @@ -163,7 +163,18 @@ def run_godot(args: list[str], desc: str, godot_bin: str, timeout_sec: int = TIM stderr = stderr_path.read_text("utf-8", errors="replace") full_output = stdout + stderr - print(full_output.rstrip()) + # ── Output printing logic ── + if verbose: + print(full_output.rstrip()) + else: + # In quiet mode: show only summary / important parts + lines = full_output.splitlines() + filtered = filter_output(lines, verbose=False) + if filtered: + print("\n".join(filtered)) + else: + print("(no relevant output)") + print(f"→ Exit code: {exit_code}") return exit_code, full_output, False @@ -172,38 +183,38 @@ def run_godot(args: list[str], desc: str, godot_bin: str, timeout_sec: int = TIM print(msg) return 1, msg, False - -def pre_import_project(godot_bin: str): +def pre_import_project(godot_bin: str, verbose: bool): print("\nPre-importing project (headless, short timeout)...") cleanup_godot_cache() args = ["--path", str(GODOT_PROJECT), "--import", "--headless"] - exit_code, output, timed_out = run_godot(args, "Pre-import", godot_bin, timeout_sec=IMPORT_TIMEOUT_SEC) + exit_code, output, timed_out = run_godot(args, "Pre-import", godot_bin, timeout_sec=IMPORT_TIMEOUT_SEC, verbose=verbose) if timed_out or exit_code != 0: - print("→ Pre-import failed/crashed — This is expected, continuing anyway") + if verbose: + print("→ Pre-import failed or timed out (full output above)") + else: + print("→ Pre-import failed/crashed — continuing anyway") else: print("→ Pre-import completed") -def run_tests(mode: str, godot_bin: str) -> bool: + +def run_tests(mode: str, godot_bin: str, verbose) -> bool: overall_success = True # ── One-time preparation ── print("\nPreparing project (one-time cleanup + pre-import)...") cleanup_godot_cache() # Clean once at start - pre_import_project(godot_bin) # Attempt import once (ignore failures) + pre_import_project(godot_bin, verbose) # Attempt import once (ignore failures) # No more cleanups after this point — let the cache persist if mode in ("unit", "full"): args = ["--path", str(GODOT_PROJECT), "--debug", "--headless", "--quit"] - _, output, timed_out = run_godot(args, "Unit / headless tests", godot_bin) - - filtered = filter_output(output.splitlines()) - print("\nFiltered output:") - print("\n".join(filtered)) + _, output, timed_out = run_godot(args, "Unit / headless tests", godot_bin, verbose=verbose) + # Summary parsing still uses full output if timed_out: print("→ Unit phase: TIMEOUT") overall_success = False @@ -213,29 +224,37 @@ def run_tests(mode: str, godot_bin: str) -> bool: else: print("→ Unit phase: detected PASSED") + if not verbose: + # Optional: print a small reminder about the known error + if "ExampleInternal" in output: + print(" (known non-fatal warning about 'ExampleInternal' suppressed)") + return overall_success # ────────────────────────────────────────────── # Main # ────────────────────────────────────────────── - def main(): parser = argparse.ArgumentParser(description="Run godot-cpp test suite (temp portable Godot)") parser.add_argument("--unit-only", action="store_const", const="unit", dest="mode") + parser.add_argument("--verbose", action="store_true", default=False, + help="Show full unfiltered Godot output + more detailed runner messages") args = parser.parse_args() mode = args.mode or "full" + verbose = args.verbose print(f"Original Godot: {ORIGINAL_GODOT}") print(f"Project: {GODOT_PROJECT}") - print(f"Mode: {mode}\n") + print(f"Mode: {mode}") + print(f"Verbose: {verbose}\n") # Setup temp portable copy godot_bin = setup_temp_portable_godot() try: - all_passed = run_tests(mode, godot_bin) + all_passed = run_tests(mode, godot_bin, verbose) finally: # Always cleanup temp files cleanup_temp_portable() From 5b6936de1b327c3d2900962030893c2d0ef2616f Mon Sep 17 00:00:00 2001 From: Samuel Nicholas Date: Fri, 6 Feb 2026 16:22:47 +1030 Subject: [PATCH 3/7] added quiet flag, more output tuning --- test/run-tests.py | 197 +++++++++++++++++++++++++++++++--------------- 1 file changed, 135 insertions(+), 62 deletions(-) diff --git a/test/run-tests.py b/test/run-tests.py index b1404c43a..c9b14e4d3 100644 --- a/test/run-tests.py +++ b/test/run-tests.py @@ -42,63 +42,75 @@ TEMP_EXE_NAME = "godot-temp-portable.exe" TEMP_MARKER_NAME = "_sc_" +PHASE_CLEANUP = 10 +PHASE_PRE_IMPORT = 20 +PHASE_UNIT_TESTS = 30 +PHASE_CLEANUP_TEMP = 40 # not really a failure point, but for completeness + # ────────────────────────────────────────────── # Portable Temp Copy Helpers # ────────────────────────────────────────────── - -def setup_temp_portable_godot() -> str: +def setup_temp_portable_godot(quiet:bool=False, verbose:bool=False) -> str: """Copy Godot exe + create marker for single-session portable mode.""" original_path = Path(ORIGINAL_GODOT).resolve() if not original_path.is_file(): - print(f"Warning: Original Godot not found at '{original_path}' — using as-is.") + if not quiet: + print(f"Warning: Original Godot not found — using as-is.") return ORIGINAL_GODOT temp_exe = Path.cwd() / TEMP_EXE_NAME temp_marker = Path.cwd() / TEMP_MARKER_NAME try: - print(f"Creating temporary portable copy: {temp_exe}") + if not quiet: + print("→ Creating portable Godot", end=" ") shutil.copy2(original_path, temp_exe) - - print(f"Creating portable marker: {temp_marker}") temp_marker.touch(exist_ok=True) - - # Return path to temp exe + if not quiet: + print("[ DONE ]") return str(temp_exe.absolute()) - except Exception as e: - print(f"Failed to setup temp portable Godot: {e}") - print("Falling back to original executable (may pollute system data dirs)") + if not quiet: + print("[ FAILED ]") + print(f"Failed to setup temp portable Godot: {e}") return ORIGINAL_GODOT -def cleanup_temp_portable(): +def cleanup_temp_portable(quiet:bool=False, verbose:bool=False): """Remove temp exe, marker, and any editor_data folder.""" temp_exe = Path.cwd() / TEMP_EXE_NAME temp_marker = Path.cwd() / TEMP_MARKER_NAME editor_data = Path.cwd() / "editor_data" + cleaned = False for path in [temp_exe, temp_marker]: if path.exists(): try: path.unlink() - print(f"Cleaned: {path}") + cleaned = True + if verbose: print(f"Cleaned: {path}") except Exception as e: print(f"Warning: Could not delete {path}: {e}") if editor_data.exists(): try: shutil.rmtree(editor_data) - print(f"Cleaned editor_data folder: {editor_data}") + if verbose: print(f"Cleaned editor_data folder: {editor_data}") except Exception as e: - print(f"Warning: Could not delete editor_data: {e}") + if verbose: print(f"Warning: Could not delete editor_data: {e}") + + if not quiet and cleaned: + print("→ Cleaned [ DONE ]") + + + # ────────────────────────────────────────────── -# Other Helpers (unchanged from previous) +# Other Helpers # ────────────────────────────────────────────── -def filter_output(lines: list[str], verbose: bool) -> list[str]: +def filter_output(lines: list[str], quiet:bool=False, verbose:bool=False) -> list[str]: if verbose: return [line.rstrip() for line in lines if line.strip()] # just trim, keep everything # original quiet filtering @@ -118,8 +130,21 @@ def is_successful(output: str) -> bool: has_failed = FAILED_MARKER in output return has_end and has_passed and not has_failed +def cleanup_godot_cache(quiet:bool=False, verbose:bool=False): + cache_dir = PROJECT_DIR / ".godot" + if cache_dir.exists(): + if not quiet: + print("→ Cleaning project cache", end=" ") + try: + shutil.rmtree(cache_dir, ignore_errors=True) + if not quiet: + print("[ DONE ]") + except Exception as e: + if verbose: + print(f"Warning: Failed to clean .godot: {e}") + elif not quiet: + print("[ FAILED ]") -def cleanup_godot_cache(): cache_dir = PROJECT_DIR / ".godot" if cache_dir.exists(): print(f"Cleaning project cache: {cache_dir}") @@ -128,8 +153,10 @@ def cleanup_godot_cache(): except Exception as e: print(f"Warning: Failed to clean .godot: {e}") - -def run_godot(args: list[str], desc: str, godot_bin: str, timeout_sec: int = TIMEOUT_SEC, verbose: bool = False) -> tuple[int, str, bool]: +def run_godot( + args: list[str], desc: str, godot_bin: str, timeout_sec: int = TIMEOUT_SEC, quiet: bool = False, + verbose: bool = False, phase_code_on_fail=None +) -> tuple[int, str, bool]: print(f"\n{'─' * 10} {desc} {'─' * 10}") print(f"→ {godot_bin} {' '.join(args)}") @@ -183,55 +210,83 @@ def run_godot(args: list[str], desc: str, godot_bin: str, timeout_sec: int = TIM print(msg) return 1, msg, False -def pre_import_project(godot_bin: str, verbose: bool): - print("\nPre-importing project (headless, short timeout)...") - cleanup_godot_cache() +def pre_import_project(godot_bin:str, quiet:bool=False, verbose:bool=False): + if verbose: + print("\nPre-importing project (headless, short timeout)...") + elif not quiet: print("→ Pre-import", end=" ") + + cleanup_godot_cache(quiet=quiet, verbose=verbose) args = ["--path", str(GODOT_PROJECT), "--import", "--headless"] - exit_code, output, timed_out = run_godot(args, "Pre-import", godot_bin, timeout_sec=IMPORT_TIMEOUT_SEC, verbose=verbose) - + exit_code, output, timed_out = run_godot( + args, "Pre-import", godot_bin, + timeout_sec=IMPORT_TIMEOUT_SEC, + verbose=verbose, quiet=quiet + ) if timed_out or exit_code != 0: if verbose: - print("→ Pre-import failed or timed out (full output above)") - else: - print("→ Pre-import failed/crashed — continuing anyway") + print("→ Pre-import failed or timed out — continuing anyway") + elif not quiet: + print("[ CRASH/FAIL ]") + return False, PHASE_PRE_IMPORT else: - print("→ Pre-import completed") + if verbose: + print("→ Pre-import completed") + elif not quiet: + print("[ DONE ]") + return True, 0 -def run_tests(mode: str, godot_bin: str, verbose) -> bool: +def run_tests(mode: str, godot_bin: str, quiet:bool=False, verbose:bool=False) -> bool: overall_success = True + failed_phase = 0 - # ── One-time preparation ── - print("\nPreparing project (one-time cleanup + pre-import)...") - cleanup_godot_cache() # Clean once at start + if not quiet: + print("Preparing project...") +# ── One-time preparation ── + if not quiet: + print("\nPreparing project (one-time cleanup + pre-import)...") + print("Preparing project...") - pre_import_project(godot_bin, verbose) # Attempt import once (ignore failures) + ok, phase = pre_import_project(godot_bin, quiet=quiet, verbose=verbose) # Attempt import once (ignore failures) + if not ok: + return False # early return — we'll set exit code higher up # No more cleanups after this point — let the cache persist if mode in ("unit", "full"): + if not quiet: print("→ Unit / headless tests") + args = ["--path", str(GODOT_PROJECT), "--debug", "--headless", "--quit"] - _, output, timed_out = run_godot(args, "Unit / headless tests", godot_bin, verbose=verbose) + exit_code, output, timed_out = run_godot( + args, "Unit / headless tests", godot_bin, + verbose=verbose, quiet=quiet ) # Summary parsing still uses full output if timed_out: - print("→ Unit phase: TIMEOUT") + if not quiet: print("→ Unit phase: TIMEOUT") overall_success = False + failed_phase = PHASE_UNIT_TESTS elif not is_successful(output): - print("→ Unit phase: did NOT detect clean success") + if not quiet: + print("→ Unit phase: did NOT detect clean success") overall_success = False - else: - print("→ Unit phase: detected PASSED") + failed_phase = PHASE_UNIT_TESTS + elif not quiet: + # Show minimal success summary even in non-verbose + lines = output.splitlines() + for line in lines: + if any(m in line for m in [END_MARKER, PASSED_MARKER]): + print(line.strip()) + print("→ Unit phase: [ PASSED ]") - if not verbose: - # Optional: print a small reminder about the known error - if "ExampleInternal" in output: - print(" (known non-fatal warning about 'ExampleInternal' suppressed)") + if verbose: + # Optional: print a small reminder about the known error + if "ExampleInternal" in output: + print(" (known non-fatal warning about 'ExampleInternal' suppressed)") return overall_success - # ────────────────────────────────────────────── # Main # ────────────────────────────────────────────── @@ -239,33 +294,51 @@ def main(): parser = argparse.ArgumentParser(description="Run godot-cpp test suite (temp portable Godot)") parser.add_argument("--unit-only", action="store_const", const="unit", dest="mode") parser.add_argument("--verbose", action="store_true", default=False, - help="Show full unfiltered Godot output + more detailed runner messages") + help="Show full unfiltered Godot output") + parser.add_argument("--quiet", action="store_true", default=False, + help="Minimal output — only final exit code (for CI)") args = parser.parse_args() - mode = args.mode or "full" + mode = args.mode or "full" verbose = args.verbose + quiet = args.quiet - print(f"Original Godot: {ORIGINAL_GODOT}") - print(f"Project: {GODOT_PROJECT}") - print(f"Mode: {mode}") - print(f"Verbose: {verbose}\n") + # ── Early exit for quiet mode if we want ultra-minimal ── + if quiet: + # We'll suppress almost all prints later + def qprint(*a, **kw): pass + global print + print = qprint # monkey-patch print (crude but effective for this script) - # Setup temp portable copy - godot_bin = setup_temp_portable_godot() + if quiet and verbose: + print("Error: --quiet and --verbose are mutually exclusive", file=sys.stderr) + sys.exit(1) + + print(f"Godot Executable: {ORIGINAL_GODOT}") + print(f"Project: {GODOT_PROJECT}") + print(f"Mode: {mode}") + print(f"Verbose: {verbose}\n") + godot_bin = setup_temp_portable_godot(quiet=quiet) + + exit_code = 0 try: - all_passed = run_tests(mode, godot_bin, verbose) + success = run_tests(mode, godot_bin, verbose=verbose, quiet=quiet) + if not success: + exit_code = 3 # default unit failure; overridden in run_tests if earlier phase + except Exception as e: + print(f"Runner crashed: {e}", file=sys.stderr) + exit_code = 1 finally: - # Always cleanup temp files - cleanup_temp_portable() + cleanup_temp_portable(quiet=quiet, verbose=verbose) - print("\n" + "═" * 40) - if all_passed: - print("TEST SUITE PASSED") - sys.exit(0) - else: - print("TEST SUITE FAILED") - sys.exit(1) + if not quiet: + print("\n" + "═" * 40) + status = "PASSED" if exit_code == 0 else f"FAILED (code {exit_code})" + duration = f" - took {int(time.time() - start_time)}s" if 'start_time' in globals() else "" + print(f"TEST SUITE {status}{duration}") + + sys.exit(exit_code) if __name__ == "__main__": From ca3d0a847cd3f328ec4b4625c21de5b1c7b8e95d Mon Sep 17 00:00:00 2001 From: Samuel Nicholas Date: Fri, 6 Feb 2026 18:01:55 +1030 Subject: [PATCH 4/7] Worked on cleaning up the output a ton to make it nicer. --- test/run-tests.py | 337 ++++++++++++++++++++-------------------------- 1 file changed, 147 insertions(+), 190 deletions(-) diff --git a/test/run-tests.py b/test/run-tests.py index c9b14e4d3..564da329b 100644 --- a/test/run-tests.py +++ b/test/run-tests.py @@ -1,12 +1,10 @@ #!/usr/bin/env python3 """ run-tests.py - Robust test runner for godot-cpp test project (with temp portable Godot copy) - -Usage: - python run-tests.py [--unit-only | --TODO-only] # default: full """ import argparse +import builtins import os import re import shutil @@ -32,11 +30,15 @@ TIMEOUT_SEC = 180 IMPORT_TIMEOUT_SEC = 30 -FILTER_PATTERNS = [ - re.compile(r"Narrowing conversion"), - re.compile(r"\[\s*\d+%\s*]"), - re.compile(r"first_scan_filesystem"), - re.compile(r"loading_editor_layout"), +FILTER_INCLUDE_PATTERNS = [ + re.compile(r"^.*={4}\s*TESTS\s*FINISHED\s*={4}"), # ==== ... ==== + re.compile(r"^.*PASSES:\s*\d+"), # PASSES: + re.compile(r"^.*FAILURES:\s*\d+"), # FAILURES: + re.compile(r"^.*\*+\s*PASSED\s*\*+"), # any number of stars around PASSED + re.compile(r"^.*\*+\s*FAILED\s*\*+"), # same for FAILED (useful for future) +] +FILTER_DISCARD_PATTERNS = [ + re.compile(r".*"), # Discard everything that hasnt already been included. ] TEMP_EXE_NAME = "godot-temp-portable.exe" @@ -45,39 +47,52 @@ PHASE_CLEANUP = 10 PHASE_PRE_IMPORT = 20 PHASE_UNIT_TESTS = 30 -PHASE_CLEANUP_TEMP = 40 # not really a failure point, but for completeness # ────────────────────────────────────────────── -# Portable Temp Copy Helpers +# Helpers # ────────────────────────────────────────────── -def setup_temp_portable_godot(quiet:bool=False, verbose:bool=False) -> str: - """Copy Godot exe + create marker for single-session portable mode.""" + +def filter_output(lines: list[str]) -> list[str]: + result = [] + for line in lines: + cleaned = line.rstrip() + if not cleaned: continue + if any(pat.search(cleaned) for pat in FILTER_INCLUDE_PATTERNS): + result.append(cleaned) + continue + if any(pat.search(cleaned) for pat in FILTER_DISCARD_PATTERNS): + continue + result.append(cleaned) + return result + +def is_successful(output: str) -> bool: + return END_MARKER in output and PASSED_MARKER in output and FAILED_MARKER not in output + +# ────────────────────────────────────────────── +# Portable Godot +# ────────────────────────────────────────────── + +def setup_temp_portable_godot(): original_path = Path(ORIGINAL_GODOT).resolve() if not original_path.is_file(): - if not quiet: - print(f"Warning: Original Godot not found — using as-is.") + print(f"Warning: Original Godot not found — using as-is.") return ORIGINAL_GODOT temp_exe = Path.cwd() / TEMP_EXE_NAME temp_marker = Path.cwd() / TEMP_MARKER_NAME try: - if not quiet: - print("→ Creating portable Godot", end=" ") + print("→ Creating portable Godot", end=" ") shutil.copy2(original_path, temp_exe) temp_marker.touch(exist_ok=True) - if not quiet: - print("[ DONE ]") + print("[ DONE ]") return str(temp_exe.absolute()) except Exception as e: - if not quiet: - print("[ FAILED ]") - print(f"Failed to setup temp portable Godot: {e}") + print("[ FAILED ]") return ORIGINAL_GODOT -def cleanup_temp_portable(quiet:bool=False, verbose:bool=False): - """Remove temp exe, marker, and any editor_data folder.""" +def cleanup_temp_portable(): temp_exe = Path.cwd() / TEMP_EXE_NAME temp_marker = Path.cwd() / TEMP_MARKER_NAME editor_data = Path.cwd() / "editor_data" @@ -88,83 +103,53 @@ def cleanup_temp_portable(quiet:bool=False, verbose:bool=False): try: path.unlink() cleaned = True - if verbose: print(f"Cleaned: {path}") - except Exception as e: - print(f"Warning: Could not delete {path}: {e}") - + except: + pass if editor_data.exists(): try: shutil.rmtree(editor_data) - if verbose: print(f"Cleaned editor_data folder: {editor_data}") - except Exception as e: - if verbose: print(f"Warning: Could not delete editor_data: {e}") - - if not quiet and cleaned: - print("→ Cleaned [ DONE ]") - - + cleaned = True + except: + pass + if cleaned: print("→ Cleaned [ DONE ]") # ────────────────────────────────────────────── -# Other Helpers +# Cache & Run # ────────────────────────────────────────────── -def filter_output(lines: list[str], quiet:bool=False, verbose:bool=False) -> list[str]: - if verbose: - return [line.rstrip() for line in lines if line.strip()] # just trim, keep everything - # original quiet filtering - result = [] - for line in lines: - cleaned = line.rstrip() - if not cleaned: - continue - if any(pat.search(cleaned) for pat in FILTER_PATTERNS): - continue - result.append(cleaned) - return result - -def is_successful(output: str) -> bool: - has_end = END_MARKER in output - has_passed = PASSED_MARKER in output - has_failed = FAILED_MARKER in output - return has_end and has_passed and not has_failed - -def cleanup_godot_cache(quiet:bool=False, verbose:bool=False): +def cleanup_godot_cache(verbose:bool=False) -> bool: cache_dir = PROJECT_DIR / ".godot" if cache_dir.exists(): - if not quiet: - print("→ Cleaning project cache", end=" ") + print("→ Cleaning project cache", end=" ") try: shutil.rmtree(cache_dir, ignore_errors=True) - if not quiet: - print("[ DONE ]") + print("[ DONE ]") except Exception as e: - if verbose: - print(f"Warning: Failed to clean .godot: {e}") - elif not quiet: - print("[ FAILED ]") + print(f"[ FAILED ] {e}") + return False + return True - cache_dir = PROJECT_DIR / ".godot" - if cache_dir.exists(): - print(f"Cleaning project cache: {cache_dir}") - try: - shutil.rmtree(cache_dir, ignore_errors=True) - except Exception as e: - print(f"Warning: Failed to clean .godot: {e}") def run_godot( - args: list[str], desc: str, godot_bin: str, timeout_sec: int = TIMEOUT_SEC, quiet: bool = False, - verbose: bool = False, phase_code_on_fail=None -) -> tuple[int, str, bool]: - print(f"\n{'─' * 10} {desc} {'─' * 10}") - print(f"→ {godot_bin} {' '.join(args)}") + args: list[str], + desc: str, + godot_bin: str, + timeout_sec: int = TIMEOUT_SEC, + verbose: bool = False, +) -> tuple[int, str, str, str]: + + if verbose: + print(f"\n{'─' * 10} {desc} {'─' * 10}") + print(f"→ {godot_bin} {' '.join(args)}") with tempfile.TemporaryDirectory() as tmpdir: stdout_path = Path(tmpdir) / "stdout.txt" stderr_path = Path(tmpdir) / "stderr.txt" cmd = [godot_bin] + args + try: start = time.time() proc = subprocess.Popen( @@ -174,171 +159,143 @@ def run_godot( cwd=os.getcwd(), start_new_session=True, ) + + timeout = False while proc.poll() is None: if time.time() - start > timeout_sec: - print(f"→ TIMEOUT after {timeout_sec}s – killing") proc.send_signal(signal.SIGTERM) time.sleep(1) if proc.poll() is None: proc.kill() proc.wait() - return 124, "TIMEOUT", True - time.sleep(0.5) + timeout=True + time.sleep(0.3) exit_code = proc.returncode stdout = stdout_path.read_text("utf-8", errors="replace") stderr = stderr_path.read_text("utf-8", errors="replace") full_output = stdout + stderr - # ── Output printing logic ── if verbose: print(full_output.rstrip()) - else: - # In quiet mode: show only summary / important parts - lines = full_output.splitlines() - filtered = filter_output(lines, verbose=False) - if filtered: - print("\n".join(filtered)) - else: - print("(no relevant output)") - - print(f"→ Exit code: {exit_code}") - return exit_code, full_output, False + print(f"→ Exit code: {exit_code}") + + if timeout: + return 124, "TIMEOUT", f'After {timeout_sec}s', full_output + + return exit_code, "DONE", f'Exit code: {exit_code}', full_output except Exception as exc: - msg = f"Failed to run Godot: {exc}" - print(msg) - return 1, msg, False + stdout = stdout_path.read_text("utf-8", errors="replace") + stderr = stderr_path.read_text("utf-8", errors="replace") + full_output = stdout + stderr + + if verbose: + print(f"Failed to run Godot: {exc}") + print(full_output.rstrip()) + return 1, "EXCEPTION", f"{exc}", full_output -def pre_import_project(godot_bin:str, quiet:bool=False, verbose:bool=False): - if verbose: - print("\nPre-importing project (headless, short timeout)...") - elif not quiet: print("→ Pre-import", end=" ") - cleanup_godot_cache(quiet=quiet, verbose=verbose) +def pre_import_project(godot_bin: str, verbose: bool = False): + if not verbose: print("→ Pre-Import", end=" ", flush=True) args = ["--path", str(GODOT_PROJECT), "--import", "--headless"] - exit_code, output, timed_out = run_godot( - args, "Pre-import", godot_bin, - timeout_sec=IMPORT_TIMEOUT_SEC, - verbose=verbose, quiet=quiet + exit_code, strcode, msg, output = run_godot( + args, "Pre-import", godot_bin, timeout_sec=IMPORT_TIMEOUT_SEC, + verbose=verbose ) - if timed_out or exit_code != 0: - if verbose: - print("→ Pre-import failed or timed out — continuing anyway") - elif not quiet: - print("[ CRASH/FAIL ]") - return False, PHASE_PRE_IMPORT - else: - if verbose: - print("→ Pre-import completed") - elif not quiet: - print("[ DONE ]") - return True, 0 + if not verbose: + # Show only summary / important parts + lines = output.splitlines() + filtered = filter_output(lines) + if filtered: print("\n".join(filtered)) + print(f"[ {strcode} ]", end=' ') + print(f"- {msg}" if msg else '') + return exit_code != 0 -def run_tests(mode: str, godot_bin: str, quiet:bool=False, verbose:bool=False) -> bool: - overall_success = True - failed_phase = 0 - if not quiet: - print("Preparing project...") -# ── One-time preparation ── - if not quiet: - print("\nPreparing project (one-time cleanup + pre-import)...") - print("Preparing project...") - - ok, phase = pre_import_project(godot_bin, quiet=quiet, verbose=verbose) # Attempt import once (ignore failures) - if not ok: - return False # early return — we'll set exit code higher up - - # No more cleanups after this point — let the cache persist +def run_tests(mode: str, godot_bin: str, verbose: bool = False) -> bool: + success = True if mode in ("unit", "full"): - if not quiet: print("→ Unit / headless tests") + print("→ Unit/Integration Tests", end=' ', flush=True) args = ["--path", str(GODOT_PROJECT), "--debug", "--headless", "--quit"] - exit_code, output, timed_out = run_godot( - args, "Unit / headless tests", godot_bin, - verbose=verbose, quiet=quiet ) - - # Summary parsing still uses full output - if timed_out: - if not quiet: print("→ Unit phase: TIMEOUT") - overall_success = False - failed_phase = PHASE_UNIT_TESTS + exitcode, strcode, msg, output = run_godot( + args, "Unit tests", godot_bin, verbose=verbose + ) + + if not verbose: + print(f"[ {strcode} ]", end=' ') + print(f"- {msg}" if msg else '') + + if exitcode == 127: + print("→ Unit phase: TIMEOUT") + success = False elif not is_successful(output): - if not quiet: - print("→ Unit phase: did NOT detect clean success") - overall_success = False - failed_phase = PHASE_UNIT_TESTS - elif not quiet: - # Show minimal success summary even in non-verbose - lines = output.splitlines() - for line in lines: - if any(m in line for m in [END_MARKER, PASSED_MARKER]): - print(line.strip()) - print("→ Unit phase: [ PASSED ]") - - if verbose: - # Optional: print a small reminder about the known error - if "ExampleInternal" in output: - print(" (known non-fatal warning about 'ExampleInternal' suppressed)") - - return overall_success + print("→ Unit phase: FAILED") + success = False + + return success + # ────────────────────────────────────────────── # Main # ────────────────────────────────────────────── + def main(): - parser = argparse.ArgumentParser(description="Run godot-cpp test suite (temp portable Godot)") + parser = argparse.ArgumentParser(description="Run godot-cpp test suite") parser.add_argument("--unit-only", action="store_const", const="unit", dest="mode") parser.add_argument("--verbose", action="store_true", default=False, help="Show full unfiltered Godot output") parser.add_argument("--quiet", action="store_true", default=False, - help="Minimal output — only final exit code (for CI)") + help="Only exit code (0=success, >0=failure); no output") args = parser.parse_args() + # store a reference to print + original_print = builtins.print + mode = args.mode or "full" verbose = args.verbose - quiet = args.quiet - # ── Early exit for quiet mode if we want ultra-minimal ── - if quiet: - # We'll suppress almost all prints later - def qprint(*a, **kw): pass - global print - print = qprint # monkey-patch print (crude but effective for this script) + if args.quiet: + def silent(*_args, **_kwargs): + pass + builtins.print = silent + else: + builtins.print = original_print # restore just in case + + if args.quiet and args.verbose: + print("--quiet takes precedence over --verbose", file=sys.stderr) + verbose = False - if quiet and verbose: - print("Error: --quiet and --verbose are mutually exclusive", file=sys.stderr) - sys.exit(1) + start_time = time.time() print(f"Godot Executable: {ORIGINAL_GODOT}") print(f"Project: {GODOT_PROJECT}") print(f"Mode: {mode}") print(f"Verbose: {verbose}\n") - godot_bin = setup_temp_portable_godot(quiet=quiet) + godot_bin = setup_temp_portable_godot() + # TODO test godot bin to make sure its ok. - exit_code = 0 - try: - success = run_tests(mode, godot_bin, verbose=verbose, quiet=quiet) - if not success: - exit_code = 3 # default unit failure; overridden in run_tests if earlier phase - except Exception as e: - print(f"Runner crashed: {e}", file=sys.stderr) - exit_code = 1 - finally: - cleanup_temp_portable(quiet=quiet, verbose=verbose) - - if not quiet: - print("\n" + "═" * 40) - status = "PASSED" if exit_code == 0 else f"FAILED (code {exit_code})" - duration = f" - took {int(time.time() - start_time)}s" if 'start_time' in globals() else "" - print(f"TEST SUITE {status}{duration}") - - sys.exit(exit_code) + pre_clean = cleanup_godot_cache(verbose=verbose) + + pre_import = pre_import_project(godot_bin, verbose=verbose) + + success = run_tests(mode, godot_bin, verbose=verbose) + + cleanup_temp_portable() + + duration = int(time.time() - start_time) + + print("" + "-" * 80) + status = "PASSED" if success else "FAILED" + print(f"TEST SUITE {status} - took {duration}s") + + builtins.print = original_print + sys.exit(0 if success else 3) if __name__ == "__main__": From 99b4750e49e2e183d0f9fc6f339918d7ea263ccd Mon Sep 17 00:00:00 2001 From: Samuel Nicholas Date: Fri, 6 Feb 2026 20:08:36 +1030 Subject: [PATCH 5/7] added xml docgen to script. --- test/run-tests.py | 131 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 101 insertions(+), 30 deletions(-) diff --git a/test/run-tests.py b/test/run-tests.py index 564da329b..77e1ff79b 100644 --- a/test/run-tests.py +++ b/test/run-tests.py @@ -65,15 +65,11 @@ def filter_output(lines: list[str]) -> list[str]: result.append(cleaned) return result -def is_successful(output: str) -> bool: - return END_MARKER in output and PASSED_MARKER in output and FAILED_MARKER not in output - # ────────────────────────────────────────────── # Portable Godot # ────────────────────────────────────────────── -def setup_temp_portable_godot(): - original_path = Path(ORIGINAL_GODOT).resolve() +def setup_temp_portable_godot( original_path:Path ): if not original_path.is_file(): print(f"Warning: Original Godot not found — using as-is.") return ORIGINAL_GODOT @@ -178,12 +174,12 @@ def run_godot( if verbose: print(full_output.rstrip()) - print(f"→ Exit code: {exit_code}") + print(f"\n{'─' * 10} {desc} - exit:{exit_code:#x} {'─' * 10}") if timeout: return 124, "TIMEOUT", f'After {timeout_sec}s', full_output - return exit_code, "DONE", f'Exit code: {exit_code}', full_output + return exit_code, "DONE", f'Exit code: {exit_code:#x}', full_output except Exception as exc: stdout = stdout_path.read_text("utf-8", errors="replace") @@ -215,30 +211,68 @@ def pre_import_project(godot_bin: str, verbose: bool = False): return exit_code != 0 -def run_tests(mode: str, godot_bin: str, verbose: bool = False) -> bool: - success = True +def run_integration_tests(godot_bin: str, verbose: bool = False) -> bool: + print("→ Unit/Integration Tests", end=' ', flush=True) - if mode in ("unit", "full"): - print("→ Unit/Integration Tests", end=' ', flush=True) + args = [ + "--path", str(GODOT_PROJECT), + "--debug", "--headless", "--quit"] + exitcode, strcode, msg, output = run_godot( + args, "Unit/Integration tests", godot_bin, verbose=verbose + ) - args = ["--path", str(GODOT_PROJECT), "--debug", "--headless", "--quit"] - exitcode, strcode, msg, output = run_godot( - args, "Unit tests", godot_bin, verbose=verbose - ) + def is_successful(output: str) -> bool: + return END_MARKER in output and PASSED_MARKER in output and FAILED_MARKER not in output - if not verbose: - print(f"[ {strcode} ]", end=' ') - print(f"- {msg}" if msg else '') + if not verbose: + print(f"[ {strcode} ]", end=' ') + print(f"- {msg}" if msg else '') - if exitcode == 127: - print("→ Unit phase: TIMEOUT") - success = False - elif not is_successful(output): - print("→ Unit phase: FAILED") - success = False + if exitcode == 127: + print("→ Unit phase: TIMEOUT") + return False + elif not is_successful(output): + print("→ Unit phase: FAILED") + return False + else: + return True - return success +def generate_extension_docs(godot_bin: str, verbose: bool = False) -> bool: + print("→ GDExtension XML DocGen", end=' ', flush=True) + + # Run from inside project/ (demo/), pointing --doctool at ../ + args = [ + "--path", str(PROJECT_DIR), + "--doctool", "..", "--gdextension-docs", + "--headless", "--quit", + ] + exitcode, strcode, msg, output = run_godot( + args, "GDExtension XML DocGen", godot_bin, verbose=verbose + ) + + # print the completion of the non verbose line. + if not verbose: + print(f"[ {strcode} ]", end=' ') + print(f"- {msg}" if msg else '') + + if strcode == 'TIMEOUT': + if verbose: print("→ DocGen phase: TIMEOUT") + return False + + doc_path = (PROJECT_DIR.parent / "doc_classes").resolve() + if doc_path.exists(): + xml_files = list(doc_path.glob('*.xml')) + if len(xml_files) > 0: + if verbose: + print(f"→ DocGen doc_classes/ created at: {doc_path} ({len(xml_files)} XML files)") + for file in xml_files: print(file) + return True + if verbose: print("→ Warning: DocGen Command succeeded but no doc_classes/*.xml found") + return False + else: + print("→ DocGen phase: FAILED") + return False # ────────────────────────────────────────────── # Main @@ -246,19 +280,29 @@ def run_tests(mode: str, godot_bin: str, verbose: bool = False) -> bool: def main(): parser = argparse.ArgumentParser(description="Run godot-cpp test suite") - parser.add_argument("--unit-only", action="store_const", const="unit", dest="mode") + parser.add_argument("--tests-only", action="store_const", const="unit", dest="mode", + help="Only run the integration tests (skip doc xml generation)") + parser.add_argument("--docs-only", action="store_const", const="docs", dest="mode", + help="Only generate GDExtension XML documentation (skip tests)") parser.add_argument("--verbose", action="store_true", default=False, help="Show full unfiltered Godot output") parser.add_argument("--quiet", action="store_true", default=False, help="Only exit code (0=success, >0=failure); no output") + parser.add_argument("--editor-bin", default=ORIGINAL_GODOT, + help="Path to Godot editor binary for --doctool (default: same as test Godot)") args = parser.parse_args() # store a reference to print original_print = builtins.print + godot_path = Path(ORIGINAL_GODOT).resolve() + editor_path = Path(args.editor_bin).resolve() + # TODO test godot bin to make sure its ok. + mode = args.mode or "full" verbose = args.verbose + if args.quiet: def silent(*_args, **_kwargs): pass @@ -277,14 +321,19 @@ def silent(*_args, **_kwargs): print(f"Mode: {mode}") print(f"Verbose: {verbose}\n") - godot_bin = setup_temp_portable_godot() - # TODO test godot bin to make sure its ok. + godot_bin = setup_temp_portable_godot(godot_path) pre_clean = cleanup_godot_cache(verbose=verbose) - pre_import = pre_import_project(godot_bin, verbose=verbose) + # NOTE: the above arent strictly necessary , and the import will always fail anyway. - success = run_tests(mode, godot_bin, verbose=verbose) + success = True + if mode in ("unit", "full") and success: + success = run_integration_tests(godot_bin, verbose=verbose) + + if mode in ("docs", "full") and success: + editor_bin = godot_bin if editor_path == godot_path else setup_temp_portable_godot(editor_path) + success = generate_extension_docs(editor_bin, verbose=verbose) cleanup_temp_portable() @@ -300,3 +349,25 @@ def silent(*_args, **_kwargs): if __name__ == "__main__": main() + +# +# try: +# godot_bin = setup_temp_portable_godot() # still use temp copy for tests +# +# if args.generate_docs_only: +# # Standalone mode — use possibly different editor binary +# editor_bin = args.godot_editor +# success = generate_extension_docs(editor_bin) +# else: +# # Normal test run +# all_passed = run_tests(mode, godot_bin) +# +# if all_passed and mode == "full": # only do docs in full mode, after tests pass +# print("\nAll tests passed → generating GDExtension docs as final step...") +# success = generate_extension_docs(godot_bin) # reuse test Godot binary +# all_passed = all_passed and success +# else: +# success = all_passed # no docs run → status is just tests +# +# finally: +# cleanup_temp_portable() From 8185b25702a45c1831bb2f0e57cc5ec09311fa87 Mon Sep 17 00:00:00 2001 From: Samuel Nicholas Date: Fri, 6 Feb 2026 20:22:55 +1030 Subject: [PATCH 6/7] erase commented out code --- test/run-tests.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/test/run-tests.py b/test/run-tests.py index 77e1ff79b..e2983c934 100644 --- a/test/run-tests.py +++ b/test/run-tests.py @@ -349,25 +349,3 @@ def silent(*_args, **_kwargs): if __name__ == "__main__": main() - -# -# try: -# godot_bin = setup_temp_portable_godot() # still use temp copy for tests -# -# if args.generate_docs_only: -# # Standalone mode — use possibly different editor binary -# editor_bin = args.godot_editor -# success = generate_extension_docs(editor_bin) -# else: -# # Normal test run -# all_passed = run_tests(mode, godot_bin) -# -# if all_passed and mode == "full": # only do docs in full mode, after tests pass -# print("\nAll tests passed → generating GDExtension docs as final step...") -# success = generate_extension_docs(godot_bin) # reuse test Godot binary -# all_passed = all_passed and success -# else: -# success = all_passed # no docs run → status is just tests -# -# finally: -# cleanup_temp_portable() From d04929ede7ac496251b696cb149f7efe06ef7e0a Mon Sep 17 00:00:00 2001 From: Samuel Nicholas Date: Fri, 6 Feb 2026 20:47:03 +1030 Subject: [PATCH 7/7] make pre-commit happy --- test/run-tests.py | 174 ++++++++++++++++++++++++++-------------------- 1 file changed, 100 insertions(+), 74 deletions(-) diff --git a/test/run-tests.py b/test/run-tests.py index e2983c934..74a5979ab 100644 --- a/test/run-tests.py +++ b/test/run-tests.py @@ -14,6 +14,7 @@ import tempfile import time from pathlib import Path +from typing import List, Tuple # ────────────────────────────────────────────── # Configuration @@ -23,40 +24,42 @@ PROJECT_DIR = Path("project").resolve() GODOT_PROJECT = PROJECT_DIR -END_MARKER = "==== TESTS FINISHED ====" +END_MARKER = "==== TESTS FINISHED ====" PASSED_MARKER = "******** PASSED ********" FAILED_MARKER = "******** FAILED ********" -TIMEOUT_SEC = 180 +TIMEOUT_SEC = 180 IMPORT_TIMEOUT_SEC = 30 FILTER_INCLUDE_PATTERNS = [ - re.compile(r"^.*={4}\s*TESTS\s*FINISHED\s*={4}"), # ==== ... ==== - re.compile(r"^.*PASSES:\s*\d+"), # PASSES: - re.compile(r"^.*FAILURES:\s*\d+"), # FAILURES: - re.compile(r"^.*\*+\s*PASSED\s*\*+"), # any number of stars around PASSED - re.compile(r"^.*\*+\s*FAILED\s*\*+"), # same for FAILED (useful for future) + re.compile(r"^.*={4}\s*TESTS\s*FINISHED\s*={4}"), # ==== ... ==== + re.compile(r"^.*PASSES:\s*\d+"), # PASSES: + re.compile(r"^.*FAILURES:\s*\d+"), # FAILURES: + re.compile(r"^.*\*+\s*PASSED\s*\*+"), # any number of stars around PASSED + re.compile(r"^.*\*+\s*FAILED\s*\*+"), # same for FAILED (useful for future) ] FILTER_DISCARD_PATTERNS = [ - re.compile(r".*"), # Discard everything that hasnt already been included. + re.compile(r".*"), # Discard everything that hasn't already been included. ] TEMP_EXE_NAME = "godot-temp-portable.exe" TEMP_MARKER_NAME = "_sc_" -PHASE_CLEANUP = 10 -PHASE_PRE_IMPORT = 20 -PHASE_UNIT_TESTS = 30 +PHASE_CLEANUP = 10 +PHASE_PRE_IMPORT = 20 +PHASE_UNIT_TESTS = 30 # ────────────────────────────────────────────── # Helpers # ────────────────────────────────────────────── -def filter_output(lines: list[str]) -> list[str]: + +def filter_output(lines: List[str]) -> List[str]: result = [] for line in lines: cleaned = line.rstrip() - if not cleaned: continue + if not cleaned: + continue if any(pat.search(cleaned) for pat in FILTER_INCLUDE_PATTERNS): result.append(cleaned) continue @@ -65,13 +68,15 @@ def filter_output(lines: list[str]) -> list[str]: result.append(cleaned) return result + # ────────────────────────────────────────────── # Portable Godot # ────────────────────────────────────────────── -def setup_temp_portable_godot( original_path:Path ): + +def setup_temp_portable_godot(original_path: Path): if not original_path.is_file(): - print(f"Warning: Original Godot not found — using as-is.") + print("Warning: Original Godot not found — using as-is.") return ORIGINAL_GODOT temp_exe = Path.cwd() / TEMP_EXE_NAME @@ -83,12 +88,12 @@ def setup_temp_portable_godot( original_path:Path ): temp_marker.touch(exist_ok=True) print("[ DONE ]") return str(temp_exe.absolute()) - except Exception as e: + except OSError: print("[ FAILED ]") return ORIGINAL_GODOT -def cleanup_temp_portable(): +def cleanup_temp_portable(verbose: bool = False): temp_exe = Path.cwd() / TEMP_EXE_NAME temp_marker = Path.cwd() / TEMP_MARKER_NAME editor_data = Path.cwd() / "editor_data" @@ -99,23 +104,27 @@ def cleanup_temp_portable(): try: path.unlink() cleaned = True - except: - pass + except OSError: + if verbose: + print(f"→ Failed to remove {path}") if editor_data.exists(): try: shutil.rmtree(editor_data) cleaned = True - except: - pass + except OSError: + if verbose: + print("→ Failed to clean temporary editor_data directory") - if cleaned: print("→ Cleaned [ DONE ]") + if cleaned: + print("→ Cleaned [ DONE ]") # ────────────────────────────────────────────── # Cache & Run # ────────────────────────────────────────────── -def cleanup_godot_cache(verbose:bool=False) -> bool: + +def cleanup_godot_cache(verbose: bool = False) -> bool: cache_dir = PROJECT_DIR / ".godot" if cache_dir.exists(): print("→ Cleaning project cache", end=" ") @@ -129,13 +138,12 @@ def cleanup_godot_cache(verbose:bool=False) -> bool: def run_godot( - args: list[str], + args: List[str], desc: str, godot_bin: str, timeout_sec: int = TIMEOUT_SEC, verbose: bool = False, -) -> tuple[int, str, str, str]: - +) -> Tuple[int, str, str, str]: if verbose: print(f"\n{'─' * 10} {desc} {'─' * 10}") print(f"→ {godot_bin} {' '.join(args)}") @@ -164,7 +172,7 @@ def run_godot( if proc.poll() is None: proc.kill() proc.wait() - timeout=True + timeout = True time.sleep(0.3) exit_code = proc.returncode @@ -177,9 +185,9 @@ def run_godot( print(f"\n{'─' * 10} {desc} - exit:{exit_code:#x} {'─' * 10}") if timeout: - return 124, "TIMEOUT", f'After {timeout_sec}s', full_output + return 124, "TIMEOUT", f"After {timeout_sec}s", full_output - return exit_code, "DONE", f'Exit code: {exit_code:#x}', full_output + return exit_code, "DONE", f"Exit code: {exit_code:#x}", full_output except Exception as exc: stdout = stdout_path.read_text("utf-8", errors="replace") @@ -193,40 +201,37 @@ def run_godot( def pre_import_project(godot_bin: str, verbose: bool = False): - if not verbose: print("→ Pre-Import", end=" ", flush=True) + if not verbose: + print("→ Pre-Import", end=" ", flush=True) args = ["--path", str(GODOT_PROJECT), "--import", "--headless"] exit_code, strcode, msg, output = run_godot( - args, "Pre-import", godot_bin, timeout_sec=IMPORT_TIMEOUT_SEC, - verbose=verbose + args, "Pre-import", godot_bin, timeout_sec=IMPORT_TIMEOUT_SEC, verbose=verbose ) if not verbose: # Show only summary / important parts lines = output.splitlines() filtered = filter_output(lines) - if filtered: print("\n".join(filtered)) + if filtered: + print("\n".join(filtered)) - print(f"[ {strcode} ]", end=' ') - print(f"- {msg}" if msg else '') + print(f"[ {strcode} ]", end=" ") + print(f"- {msg}" if msg else "") return exit_code != 0 def run_integration_tests(godot_bin: str, verbose: bool = False) -> bool: - print("→ Unit/Integration Tests", end=' ', flush=True) + print("→ Unit/Integration Tests", end=" ", flush=True) - args = [ - "--path", str(GODOT_PROJECT), - "--debug", "--headless", "--quit"] - exitcode, strcode, msg, output = run_godot( - args, "Unit/Integration tests", godot_bin, verbose=verbose - ) + args = ["--path", str(GODOT_PROJECT), "--debug", "--headless", "--quit"] + exitcode, strcode, msg, output = run_godot(args, "Unit/Integration tests", godot_bin, verbose=verbose) def is_successful(output: str) -> bool: return END_MARKER in output and PASSED_MARKER in output and FAILED_MARKER not in output if not verbose: - print(f"[ {strcode} ]", end=' ') - print(f"- {msg}" if msg else '') + print(f"[ {strcode} ]", end=" ") + print(f"- {msg}" if msg else "") if exitcode == 127: print("→ Unit phase: TIMEOUT") @@ -239,57 +244,77 @@ def is_successful(output: str) -> bool: def generate_extension_docs(godot_bin: str, verbose: bool = False) -> bool: - print("→ GDExtension XML DocGen", end=' ', flush=True) + print("→ GDExtension XML DocGen", end=" ", flush=True) # Run from inside project/ (demo/), pointing --doctool at ../ args = [ - "--path", str(PROJECT_DIR), - "--doctool", "..", "--gdextension-docs", - "--headless", "--quit", + "--path", + str(PROJECT_DIR), + "--doctool", + "..", + "--gdextension-docs", + "--headless", + "--quit", ] - exitcode, strcode, msg, output = run_godot( - args, "GDExtension XML DocGen", godot_bin, verbose=verbose - ) + exitcode, strcode, msg, output = run_godot(args, "GDExtension XML DocGen", godot_bin, verbose=verbose) # print the completion of the non verbose line. if not verbose: - print(f"[ {strcode} ]", end=' ') - print(f"- {msg}" if msg else '') + print(f"[ {strcode} ]", end=" ") + print(f"- {msg}" if msg else "") - if strcode == 'TIMEOUT': - if verbose: print("→ DocGen phase: TIMEOUT") + if strcode == "TIMEOUT": + if verbose: + print("→ DocGen phase: TIMEOUT") return False doc_path = (PROJECT_DIR.parent / "doc_classes").resolve() if doc_path.exists(): - xml_files = list(doc_path.glob('*.xml')) + xml_files = List(doc_path.glob("*.xml")) if len(xml_files) > 0: if verbose: print(f"→ DocGen doc_classes/ created at: {doc_path} ({len(xml_files)} XML files)") - for file in xml_files: print(file) + for file in xml_files: + print(file) return True - if verbose: print("→ Warning: DocGen Command succeeded but no doc_classes/*.xml found") + if verbose: + print("→ Warning: DocGen Command succeeded but no doc_classes/*.xml found") return False else: print("→ DocGen phase: FAILED") return False + # ────────────────────────────────────────────── # Main # ────────────────────────────────────────────── + def main(): parser = argparse.ArgumentParser(description="Run godot-cpp test suite") - parser.add_argument("--tests-only", action="store_const", const="unit", dest="mode", - help="Only run the integration tests (skip doc xml generation)") - parser.add_argument("--docs-only", action="store_const", const="docs", dest="mode", - help="Only generate GDExtension XML documentation (skip tests)") - parser.add_argument("--verbose", action="store_true", default=False, - help="Show full unfiltered Godot output") - parser.add_argument("--quiet", action="store_true", default=False, - help="Only exit code (0=success, >0=failure); no output") - parser.add_argument("--editor-bin", default=ORIGINAL_GODOT, - help="Path to Godot editor binary for --doctool (default: same as test Godot)") + parser.add_argument( + "--tests-only", + action="store_const", + const="unit", + dest="mode", + help="Only run the integration tests (skip doc xml generation)", + ) + parser.add_argument( + "--docs-only", + action="store_const", + const="docs", + dest="mode", + help="Only generate GDExtension XML documentation (skip tests)", + ) + parser.add_argument("--verbose", action="store_true", default=False, help="Show full unfiltered Godot output") + parser.add_argument( + "--quiet", action="store_true", default=False, help="Only exit code (0=success, >0=failure); no output" + ) + parser.add_argument( + "--editor-bin", + default=ORIGINAL_GODOT, + help="Path to Godot editor binary for --doctool (default: same as test Godot)", + ) args = parser.parse_args() # store a reference to print @@ -299,16 +324,17 @@ def main(): editor_path = Path(args.editor_bin).resolve() # TODO test godot bin to make sure its ok. - mode = args.mode or "full" + mode = args.mode or "full" verbose = args.verbose - if args.quiet: + def silent(*_args, **_kwargs): pass + builtins.print = silent else: - builtins.print = original_print # restore just in case + builtins.print = original_print # restore just in case if args.quiet and args.verbose: print("--quiet takes precedence over --verbose", file=sys.stderr) @@ -323,9 +349,9 @@ def silent(*_args, **_kwargs): godot_bin = setup_temp_portable_godot(godot_path) - pre_clean = cleanup_godot_cache(verbose=verbose) - pre_import = pre_import_project(godot_bin, verbose=verbose) - # NOTE: the above arent strictly necessary , and the import will always fail anyway. + _ = cleanup_godot_cache(verbose=verbose) + _ = pre_import_project(godot_bin, verbose=verbose) + # NOTE: the above aren't strictly necessary , and the import will always fail anyway. success = True if mode in ("unit", "full") and success: