1414import tempfile
1515import time
1616from pathlib import Path
17+ from typing import List , Tuple
1718
1819# ──────────────────────────────────────────────
1920# Configuration
2324PROJECT_DIR = Path ("project" ).resolve ()
2425GODOT_PROJECT = PROJECT_DIR
2526
26- END_MARKER = "==== TESTS FINISHED ===="
27+ END_MARKER = "==== TESTS FINISHED ===="
2728PASSED_MARKER = "******** PASSED ********"
2829FAILED_MARKER = "******** FAILED ********"
2930
30- TIMEOUT_SEC = 180
31+ TIMEOUT_SEC = 180
3132IMPORT_TIMEOUT_SEC = 30
3233
3334FILTER_INCLUDE_PATTERNS = [
34- re .compile (r"^.*={4}\s*TESTS\s*FINISHED\s*={4}" ), # ==== ... ====
35- re .compile (r"^.*PASSES:\s*\d+" ), # PASSES: <number>
36- re .compile (r"^.*FAILURES:\s*\d+" ), # FAILURES: <number>
37- re .compile (r"^.*\*+\s*PASSED\s*\*+" ), # any number of stars around PASSED
38- re .compile (r"^.*\*+\s*FAILED\s*\*+" ), # same for FAILED (useful for future)
35+ re .compile (r"^.*={4}\s*TESTS\s*FINISHED\s*={4}" ), # ==== ... ====
36+ re .compile (r"^.*PASSES:\s*\d+" ), # PASSES: <number>
37+ re .compile (r"^.*FAILURES:\s*\d+" ), # FAILURES: <number>
38+ re .compile (r"^.*\*+\s*PASSED\s*\*+" ), # any number of stars around PASSED
39+ re .compile (r"^.*\*+\s*FAILED\s*\*+" ), # same for FAILED (useful for future)
3940]
4041FILTER_DISCARD_PATTERNS = [
41- re .compile (r".*" ), # Discard everything that hasnt already been included.
42+ re .compile (r".*" ), # Discard everything that hasn't already been included.
4243]
4344
4445TEMP_EXE_NAME = "godot-temp-portable.exe"
4546TEMP_MARKER_NAME = "_sc_"
4647
47- PHASE_CLEANUP = 10
48- PHASE_PRE_IMPORT = 20
49- PHASE_UNIT_TESTS = 30
48+ PHASE_CLEANUP = 10
49+ PHASE_PRE_IMPORT = 20
50+ PHASE_UNIT_TESTS = 30
5051
5152# ──────────────────────────────────────────────
5253# Helpers
5354# ──────────────────────────────────────────────
5455
55- def filter_output (lines : list [str ]) -> list [str ]:
56+
57+ def filter_output (lines : List [str ]) -> List [str ]:
5658 result = []
5759 for line in lines :
5860 cleaned = line .rstrip ()
59- if not cleaned : continue
61+ if not cleaned :
62+ continue
6063 if any (pat .search (cleaned ) for pat in FILTER_INCLUDE_PATTERNS ):
6164 result .append (cleaned )
6265 continue
@@ -65,13 +68,15 @@ def filter_output(lines: list[str]) -> list[str]:
6568 result .append (cleaned )
6669 return result
6770
71+
6872# ──────────────────────────────────────────────
6973# Portable Godot
7074# ──────────────────────────────────────────────
7175
72- def setup_temp_portable_godot ( original_path :Path ):
76+
77+ def setup_temp_portable_godot (original_path : Path ):
7378 if not original_path .is_file ():
74- print (f "Warning: Original Godot not found — using as-is." )
79+ print ("Warning: Original Godot not found — using as-is." )
7580 return ORIGINAL_GODOT
7681
7782 temp_exe = Path .cwd () / TEMP_EXE_NAME
@@ -83,12 +88,12 @@ def setup_temp_portable_godot( original_path:Path ):
8388 temp_marker .touch (exist_ok = True )
8489 print ("[ DONE ]" )
8590 return str (temp_exe .absolute ())
86- except Exception as e :
91+ except OSError :
8792 print ("[ FAILED ]" )
8893 return ORIGINAL_GODOT
8994
9095
91- def cleanup_temp_portable ():
96+ def cleanup_temp_portable (verbose : bool = False ):
9297 temp_exe = Path .cwd () / TEMP_EXE_NAME
9398 temp_marker = Path .cwd () / TEMP_MARKER_NAME
9499 editor_data = Path .cwd () / "editor_data"
@@ -99,23 +104,27 @@ def cleanup_temp_portable():
99104 try :
100105 path .unlink ()
101106 cleaned = True
102- except :
103- pass
107+ except OSError :
108+ if verbose :
109+ print (f"→ Failed to remove { path } " )
104110 if editor_data .exists ():
105111 try :
106112 shutil .rmtree (editor_data )
107113 cleaned = True
108- except :
109- pass
114+ except OSError :
115+ if verbose :
116+ print ("→ Failed to clean temporary editor_data directory" )
110117
111- if cleaned : print ("→ Cleaned [ DONE ]" )
118+ if cleaned :
119+ print ("→ Cleaned [ DONE ]" )
112120
113121
114122# ──────────────────────────────────────────────
115123# Cache & Run
116124# ──────────────────────────────────────────────
117125
118- def cleanup_godot_cache (verbose :bool = False ) -> bool :
126+
127+ def cleanup_godot_cache (verbose : bool = False ) -> bool :
119128 cache_dir = PROJECT_DIR / ".godot"
120129 if cache_dir .exists ():
121130 print ("→ Cleaning project cache" , end = " " )
@@ -129,13 +138,12 @@ def cleanup_godot_cache(verbose:bool=False) -> bool:
129138
130139
131140def run_godot (
132- args : list [str ],
141+ args : List [str ],
133142 desc : str ,
134143 godot_bin : str ,
135144 timeout_sec : int = TIMEOUT_SEC ,
136145 verbose : bool = False ,
137- ) -> tuple [int , str , str , str ]:
138-
146+ ) -> Tuple [int , str , str , str ]:
139147 if verbose :
140148 print (f"\n { '─' * 10 } { desc } { '─' * 10 } " )
141149 print (f"→ { godot_bin } { ' ' .join (args )} " )
@@ -164,7 +172,7 @@ def run_godot(
164172 if proc .poll () is None :
165173 proc .kill ()
166174 proc .wait ()
167- timeout = True
175+ timeout = True
168176 time .sleep (0.3 )
169177
170178 exit_code = proc .returncode
@@ -177,9 +185,9 @@ def run_godot(
177185 print (f"\n { '─' * 10 } { desc } - exit:{ exit_code :#x} { '─' * 10 } " )
178186
179187 if timeout :
180- return 124 , "TIMEOUT" , f' After { timeout_sec } s' , full_output
188+ return 124 , "TIMEOUT" , f" After { timeout_sec } s" , full_output
181189
182- return exit_code , "DONE" , f' Exit code: { exit_code :#x} ' , full_output
190+ return exit_code , "DONE" , f" Exit code: { exit_code :#x} " , full_output
183191
184192 except Exception as exc :
185193 stdout = stdout_path .read_text ("utf-8" , errors = "replace" )
@@ -193,40 +201,37 @@ def run_godot(
193201
194202
195203def pre_import_project (godot_bin : str , verbose : bool = False ):
196- if not verbose : print ("→ Pre-Import" , end = " " , flush = True )
204+ if not verbose :
205+ print ("→ Pre-Import" , end = " " , flush = True )
197206
198207 args = ["--path" , str (GODOT_PROJECT ), "--import" , "--headless" ]
199208 exit_code , strcode , msg , output = run_godot (
200- args , "Pre-import" , godot_bin , timeout_sec = IMPORT_TIMEOUT_SEC ,
201- verbose = verbose
209+ args , "Pre-import" , godot_bin , timeout_sec = IMPORT_TIMEOUT_SEC , verbose = verbose
202210 )
203211 if not verbose :
204212 # Show only summary / important parts
205213 lines = output .splitlines ()
206214 filtered = filter_output (lines )
207- if filtered : print ("\n " .join (filtered ))
215+ if filtered :
216+ print ("\n " .join (filtered ))
208217
209- print (f"[ { strcode } ]" , end = ' ' )
210- print (f"- { msg } " if msg else '' )
218+ print (f"[ { strcode } ]" , end = " " )
219+ print (f"- { msg } " if msg else "" )
211220 return exit_code != 0
212221
213222
214223def run_integration_tests (godot_bin : str , verbose : bool = False ) -> bool :
215- print ("→ Unit/Integration Tests" , end = ' ' , flush = True )
224+ print ("→ Unit/Integration Tests" , end = " " , flush = True )
216225
217- args = [
218- "--path" , str (GODOT_PROJECT ),
219- "--debug" , "--headless" , "--quit" ]
220- exitcode , strcode , msg , output = run_godot (
221- args , "Unit/Integration tests" , godot_bin , verbose = verbose
222- )
226+ args = ["--path" , str (GODOT_PROJECT ), "--debug" , "--headless" , "--quit" ]
227+ exitcode , strcode , msg , output = run_godot (args , "Unit/Integration tests" , godot_bin , verbose = verbose )
223228
224229 def is_successful (output : str ) -> bool :
225230 return END_MARKER in output and PASSED_MARKER in output and FAILED_MARKER not in output
226231
227232 if not verbose :
228- print (f"[ { strcode } ]" , end = ' ' )
229- print (f"- { msg } " if msg else '' )
233+ print (f"[ { strcode } ]" , end = " " )
234+ print (f"- { msg } " if msg else "" )
230235
231236 if exitcode == 127 :
232237 print ("→ Unit phase: TIMEOUT" )
@@ -239,57 +244,77 @@ def is_successful(output: str) -> bool:
239244
240245
241246def generate_extension_docs (godot_bin : str , verbose : bool = False ) -> bool :
242- print ("→ GDExtension XML DocGen" , end = ' ' , flush = True )
247+ print ("→ GDExtension XML DocGen" , end = " " , flush = True )
243248
244249 # Run from inside project/ (demo/), pointing --doctool at ../
245250 args = [
246- "--path" , str (PROJECT_DIR ),
247- "--doctool" , ".." , "--gdextension-docs" ,
248- "--headless" , "--quit" ,
251+ "--path" ,
252+ str (PROJECT_DIR ),
253+ "--doctool" ,
254+ ".." ,
255+ "--gdextension-docs" ,
256+ "--headless" ,
257+ "--quit" ,
249258 ]
250- exitcode , strcode , msg , output = run_godot (
251- args , "GDExtension XML DocGen" , godot_bin , verbose = verbose
252- )
259+ exitcode , strcode , msg , output = run_godot (args , "GDExtension XML DocGen" , godot_bin , verbose = verbose )
253260
254261 # print the completion of the non verbose line.
255262 if not verbose :
256- print (f"[ { strcode } ]" , end = ' ' )
257- print (f"- { msg } " if msg else '' )
263+ print (f"[ { strcode } ]" , end = " " )
264+ print (f"- { msg } " if msg else "" )
258265
259- if strcode == 'TIMEOUT' :
260- if verbose : print ("→ DocGen phase: TIMEOUT" )
266+ if strcode == "TIMEOUT" :
267+ if verbose :
268+ print ("→ DocGen phase: TIMEOUT" )
261269 return False
262270
263271 doc_path = (PROJECT_DIR .parent / "doc_classes" ).resolve ()
264272 if doc_path .exists ():
265- xml_files = list (doc_path .glob (' *.xml' ))
273+ xml_files = List (doc_path .glob (" *.xml" ))
266274 if len (xml_files ) > 0 :
267275 if verbose :
268276 print (f"→ DocGen doc_classes/ created at: { doc_path } ({ len (xml_files )} XML files)" )
269- for file in xml_files : print (file )
277+ for file in xml_files :
278+ print (file )
270279 return True
271- if verbose : print ("→ Warning: DocGen Command succeeded but no doc_classes/*.xml found" )
280+ if verbose :
281+ print ("→ Warning: DocGen Command succeeded but no doc_classes/*.xml found" )
272282 return False
273283 else :
274284 print ("→ DocGen phase: FAILED" )
275285 return False
276286
287+
277288# ──────────────────────────────────────────────
278289# Main
279290# ──────────────────────────────────────────────
280291
292+
281293def main ():
282294 parser = argparse .ArgumentParser (description = "Run godot-cpp test suite" )
283- parser .add_argument ("--tests-only" , action = "store_const" , const = "unit" , dest = "mode" ,
284- help = "Only run the integration tests (skip doc xml generation)" )
285- parser .add_argument ("--docs-only" , action = "store_const" , const = "docs" , dest = "mode" ,
286- help = "Only generate GDExtension XML documentation (skip tests)" )
287- parser .add_argument ("--verbose" , action = "store_true" , default = False ,
288- help = "Show full unfiltered Godot output" )
289- parser .add_argument ("--quiet" , action = "store_true" , default = False ,
290- help = "Only exit code (0=success, >0=failure); no output" )
291- parser .add_argument ("--editor-bin" , default = ORIGINAL_GODOT ,
292- help = "Path to Godot editor binary for --doctool (default: same as test Godot)" )
295+ parser .add_argument (
296+ "--tests-only" ,
297+ action = "store_const" ,
298+ const = "unit" ,
299+ dest = "mode" ,
300+ help = "Only run the integration tests (skip doc xml generation)" ,
301+ )
302+ parser .add_argument (
303+ "--docs-only" ,
304+ action = "store_const" ,
305+ const = "docs" ,
306+ dest = "mode" ,
307+ help = "Only generate GDExtension XML documentation (skip tests)" ,
308+ )
309+ parser .add_argument ("--verbose" , action = "store_true" , default = False , help = "Show full unfiltered Godot output" )
310+ parser .add_argument (
311+ "--quiet" , action = "store_true" , default = False , help = "Only exit code (0=success, >0=failure); no output"
312+ )
313+ parser .add_argument (
314+ "--editor-bin" ,
315+ default = ORIGINAL_GODOT ,
316+ help = "Path to Godot editor binary for --doctool (default: same as test Godot)" ,
317+ )
293318 args = parser .parse_args ()
294319
295320 # store a reference to print
@@ -299,16 +324,17 @@ def main():
299324 editor_path = Path (args .editor_bin ).resolve ()
300325 # TODO test godot bin to make sure its ok.
301326
302- mode = args .mode or "full"
327+ mode = args .mode or "full"
303328 verbose = args .verbose
304329
305-
306330 if args .quiet :
331+
307332 def silent (* _args , ** _kwargs ):
308333 pass
334+
309335 builtins .print = silent
310336 else :
311- builtins .print = original_print # restore just in case
337+ builtins .print = original_print # restore just in case
312338
313339 if args .quiet and args .verbose :
314340 print ("--quiet takes precedence over --verbose" , file = sys .stderr )
@@ -323,9 +349,9 @@ def silent(*_args, **_kwargs):
323349
324350 godot_bin = setup_temp_portable_godot (godot_path )
325351
326- pre_clean = cleanup_godot_cache (verbose = verbose )
327- pre_import = pre_import_project (godot_bin , verbose = verbose )
328- # NOTE: the above arent strictly necessary , and the import will always fail anyway.
352+ _ = cleanup_godot_cache (verbose = verbose )
353+ _ = pre_import_project (godot_bin , verbose = verbose )
354+ # NOTE: the above aren't strictly necessary , and the import will always fail anyway.
329355
330356 success = True
331357 if mode in ("unit" , "full" ) and success :
0 commit comments