Skip to content

Commit 8c1bfd7

Browse files
committed
Add the run-tests.py script which is a replication of the bash script but can be modified for cross platform later.
1 parent e83fd09 commit 8c1bfd7

File tree

1 file changed

+253
-0
lines changed

1 file changed

+253
-0
lines changed

test/run-tests.py

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
#!/usr/bin/env python3
2+
"""
3+
run-tests.py - Robust test runner for godot-cpp test project (with temp portable Godot copy)
4+
5+
Usage:
6+
python run-tests.py [--unit-only | --TODO-only] # default: full
7+
"""
8+
9+
import argparse
10+
import os
11+
import re
12+
import shutil
13+
import signal
14+
import subprocess
15+
import sys
16+
import tempfile
17+
import time
18+
from pathlib import Path
19+
20+
# ──────────────────────────────────────────────
21+
# Configuration
22+
# ──────────────────────────────────────────────
23+
24+
ORIGINAL_GODOT = os.environ.get("GODOT", "godot")
25+
PROJECT_DIR = Path("project").resolve()
26+
GODOT_PROJECT = PROJECT_DIR
27+
28+
END_MARKER = "==== TESTS FINISHED ===="
29+
PASSED_MARKER = "******** PASSED ********"
30+
FAILED_MARKER = "******** FAILED ********"
31+
32+
TIMEOUT_SEC = 180
33+
IMPORT_TIMEOUT_SEC = 30
34+
35+
FILTER_PATTERNS = [
36+
re.compile(r"Narrowing conversion"),
37+
re.compile(r"\[\s*\d+%\s*\]"),
38+
re.compile(r"first_scan_filesystem"),
39+
re.compile(r"loading_editor_layout"),
40+
]
41+
42+
TEMP_EXE_NAME = "godot-temp-portable.exe"
43+
TEMP_MARKER_NAME = "_sc_"
44+
45+
# ──────────────────────────────────────────────
46+
# Portable Temp Copy Helpers
47+
# ──────────────────────────────────────────────
48+
49+
def setup_temp_portable_godot() -> str:
50+
"""Copy Godot exe + create marker for single-session portable mode."""
51+
original_path = Path(ORIGINAL_GODOT).resolve()
52+
if not original_path.is_file():
53+
print(f"Warning: Original Godot not found at '{original_path}' — using as-is.")
54+
return ORIGINAL_GODOT
55+
56+
temp_exe = Path.cwd() / TEMP_EXE_NAME
57+
temp_marker = Path.cwd() / TEMP_MARKER_NAME
58+
59+
try:
60+
print(f"Creating temporary portable copy: {temp_exe}")
61+
shutil.copy2(original_path, temp_exe)
62+
63+
print(f"Creating portable marker: {temp_marker}")
64+
temp_marker.touch(exist_ok=True)
65+
66+
# Return path to temp exe
67+
return str(temp_exe.absolute())
68+
69+
except Exception as e:
70+
print(f"Failed to setup temp portable Godot: {e}")
71+
print("Falling back to original executable (may pollute system data dirs)")
72+
return ORIGINAL_GODOT
73+
74+
75+
def cleanup_temp_portable():
76+
"""Remove temp exe, marker, and any editor_data folder."""
77+
temp_exe = Path.cwd() / TEMP_EXE_NAME
78+
temp_marker = Path.cwd() / TEMP_MARKER_NAME
79+
editor_data = Path.cwd() / "editor_data"
80+
81+
for path in [temp_exe, temp_marker]:
82+
if path.exists():
83+
try:
84+
path.unlink()
85+
print(f"Cleaned: {path}")
86+
except Exception as e:
87+
print(f"Warning: Could not delete {path}: {e}")
88+
89+
if editor_data.exists():
90+
try:
91+
shutil.rmtree(editor_data)
92+
print(f"Cleaned editor_data folder: {editor_data}")
93+
except Exception as e:
94+
print(f"Warning: Could not delete editor_data: {e}")
95+
96+
97+
# ──────────────────────────────────────────────
98+
# Other Helpers (unchanged from previous)
99+
# ──────────────────────────────────────────────
100+
101+
def filter_output(lines: list[str]) -> list[str]:
102+
result = []
103+
for line in lines:
104+
cleaned = line.rstrip()
105+
if not cleaned:
106+
continue
107+
if any(pat.search(cleaned) for pat in FILTER_PATTERNS):
108+
continue
109+
result.append(cleaned)
110+
return result
111+
112+
113+
def is_successful(output: str) -> bool:
114+
has_end = END_MARKER in output
115+
has_passed = PASSED_MARKER in output
116+
has_failed = FAILED_MARKER in output
117+
return has_end and has_passed and not has_failed
118+
119+
120+
def cleanup_godot_cache():
121+
cache_dir = PROJECT_DIR / ".godot"
122+
if cache_dir.exists():
123+
print(f"Cleaning project cache: {cache_dir}")
124+
try:
125+
shutil.rmtree(cache_dir, ignore_errors=True)
126+
except Exception as e:
127+
print(f"Warning: Failed to clean .godot: {e}")
128+
129+
130+
def run_godot(args: list[str], desc: str, godot_bin: str, timeout_sec: int = TIMEOUT_SEC) -> tuple[int, str, bool]:
131+
print(f"\n{'─' * 10} {desc} {'─' * 10}")
132+
print(f"→ {godot_bin} {' '.join(args)}")
133+
134+
with tempfile.TemporaryDirectory() as tmpdir:
135+
stdout_path = Path(tmpdir) / "stdout.txt"
136+
stderr_path = Path(tmpdir) / "stderr.txt"
137+
138+
cmd = [godot_bin] + args
139+
140+
try:
141+
start = time.time()
142+
proc = subprocess.Popen(
143+
cmd,
144+
stdout=stdout_path.open("wb"),
145+
stderr=stderr_path.open("wb"),
146+
cwd=os.getcwd(),
147+
start_new_session=True,
148+
)
149+
150+
while proc.poll() is None:
151+
if time.time() - start > timeout_sec:
152+
print(f"→ TIMEOUT after {timeout_sec}s – killing")
153+
proc.send_signal(signal.SIGTERM)
154+
time.sleep(1)
155+
if proc.poll() is None:
156+
proc.kill()
157+
proc.wait()
158+
return 124, "TIMEOUT", True
159+
time.sleep(0.5)
160+
161+
exit_code = proc.returncode
162+
stdout = stdout_path.read_text("utf-8", errors="replace")
163+
stderr = stderr_path.read_text("utf-8", errors="replace")
164+
full_output = stdout + stderr
165+
166+
print(full_output.rstrip())
167+
print(f"→ Exit code: {exit_code}")
168+
return exit_code, full_output, False
169+
170+
except Exception as exc:
171+
msg = f"Failed to run Godot: {exc}"
172+
print(msg)
173+
return 1, msg, False
174+
175+
176+
def pre_import_project(godot_bin: str):
177+
print("\nPre-importing project (headless, short timeout)...")
178+
cleanup_godot_cache()
179+
180+
args = ["--path", str(GODOT_PROJECT), "--import", "--headless"]
181+
exit_code, output, timed_out = run_godot(args, "Pre-import", godot_bin, timeout_sec=IMPORT_TIMEOUT_SEC)
182+
183+
if timed_out or exit_code != 0:
184+
print("→ Pre-import failed/crashed — This is expected, continuing anyway")
185+
else:
186+
print("→ Pre-import completed")
187+
188+
def run_tests(mode: str, godot_bin: str) -> bool:
189+
overall_success = True
190+
191+
# ── One-time preparation ──
192+
print("\nPreparing project (one-time cleanup + pre-import)...")
193+
cleanup_godot_cache() # Clean once at start
194+
195+
pre_import_project(godot_bin) # Attempt import once (ignore failures)
196+
197+
# No more cleanups after this point — let the cache persist
198+
199+
if mode in ("unit", "full"):
200+
args = ["--path", str(GODOT_PROJECT), "--debug", "--headless", "--quit"]
201+
_, output, timed_out = run_godot(args, "Unit / headless tests", godot_bin)
202+
203+
filtered = filter_output(output.splitlines())
204+
print("\nFiltered output:")
205+
print("\n".join(filtered))
206+
207+
if timed_out:
208+
print("→ Unit phase: TIMEOUT")
209+
overall_success = False
210+
elif not is_successful(output):
211+
print("→ Unit phase: did NOT detect clean success")
212+
overall_success = False
213+
else:
214+
print("→ Unit phase: detected PASSED")
215+
216+
return overall_success
217+
218+
219+
# ──────────────────────────────────────────────
220+
# Main
221+
# ──────────────────────────────────────────────
222+
223+
def main():
224+
parser = argparse.ArgumentParser(description="Run godot-cpp test suite (temp portable Godot)")
225+
parser.add_argument("--unit-only", action="store_const", const="unit", dest="mode")
226+
args = parser.parse_args()
227+
228+
mode = args.mode or "full"
229+
230+
print(f"Original Godot: {ORIGINAL_GODOT}")
231+
print(f"Project: {GODOT_PROJECT}")
232+
print(f"Mode: {mode}\n")
233+
234+
# Setup temp portable copy
235+
godot_bin = setup_temp_portable_godot()
236+
237+
try:
238+
all_passed = run_tests(mode, godot_bin)
239+
finally:
240+
# Always cleanup temp files
241+
cleanup_temp_portable()
242+
243+
print("\n" + "═" * 40)
244+
if all_passed:
245+
print("TEST SUITE PASSED")
246+
sys.exit(0)
247+
else:
248+
print("TEST SUITE FAILED")
249+
sys.exit(1)
250+
251+
252+
if __name__ == "__main__":
253+
main()

0 commit comments

Comments
 (0)