4242TEMP_EXE_NAME = "godot-temp-portable.exe"
4343TEMP_MARKER_NAME = "_sc_"
4444
45+ PHASE_CLEANUP = 10
46+ PHASE_PRE_IMPORT = 20
47+ PHASE_UNIT_TESTS = 30
48+ PHASE_CLEANUP_TEMP = 40 # not really a failure point, but for completeness
49+
4550# ──────────────────────────────────────────────
4651# Portable Temp Copy Helpers
4752# ──────────────────────────────────────────────
48-
49- def setup_temp_portable_godot () -> str :
53+ def setup_temp_portable_godot (quiet :bool = False , verbose :bool = False ) -> str :
5054 """Copy Godot exe + create marker for single-session portable mode."""
5155 original_path = Path (ORIGINAL_GODOT ).resolve ()
5256 if not original_path .is_file ():
53- print (f"Warning: Original Godot not found at '{ original_path } ' — using as-is." )
57+ if not quiet :
58+ print (f"Warning: Original Godot not found — using as-is." )
5459 return ORIGINAL_GODOT
5560
5661 temp_exe = Path .cwd () / TEMP_EXE_NAME
5762 temp_marker = Path .cwd () / TEMP_MARKER_NAME
5863
5964 try :
60- print (f"Creating temporary portable copy: { temp_exe } " )
65+ if not quiet :
66+ print ("→ Creating portable Godot" , end = " " )
6167 shutil .copy2 (original_path , temp_exe )
62-
63- print (f"Creating portable marker: { temp_marker } " )
6468 temp_marker .touch (exist_ok = True )
65-
66- # Return path to temp exe
69+ if not quiet :
70+ print ( "[ DONE ]" )
6771 return str (temp_exe .absolute ())
68-
6972 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)" )
73+ if not quiet :
74+ print ("[ FAILED ]" )
75+ print (f"Failed to setup temp portable Godot: { e } " )
7276 return ORIGINAL_GODOT
7377
7478
75- def cleanup_temp_portable ():
79+ def cleanup_temp_portable (quiet : bool = False , verbose : bool = False ):
7680 """Remove temp exe, marker, and any editor_data folder."""
7781 temp_exe = Path .cwd () / TEMP_EXE_NAME
7882 temp_marker = Path .cwd () / TEMP_MARKER_NAME
7983 editor_data = Path .cwd () / "editor_data"
8084
85+ cleaned = False
8186 for path in [temp_exe , temp_marker ]:
8287 if path .exists ():
8388 try :
8489 path .unlink ()
85- print (f"Cleaned: { path } " )
90+ cleaned = True
91+ if verbose : print (f"Cleaned: { path } " )
8692 except Exception as e :
8793 print (f"Warning: Could not delete { path } : { e } " )
8894
8995 if editor_data .exists ():
9096 try :
9197 shutil .rmtree (editor_data )
92- print (f"Cleaned editor_data folder: { editor_data } " )
98+ if verbose : print (f"Cleaned editor_data folder: { editor_data } " )
9399 except Exception as e :
94- print (f"Warning: Could not delete editor_data: { e } " )
100+ if verbose : print (f"Warning: Could not delete editor_data: { e } " )
101+
102+ if not quiet and cleaned :
103+ print ("→ Cleaned [ DONE ]" )
104+
105+
106+
95107
96108
97109# ──────────────────────────────────────────────
98- # Other Helpers (unchanged from previous)
110+ # Other Helpers
99111# ──────────────────────────────────────────────
100112
101- def filter_output (lines : list [str ], verbose : bool ) -> list [str ]:
113+ def filter_output (lines : list [str ], quiet : bool = False , verbose : bool = False ) -> list [str ]:
102114 if verbose :
103115 return [line .rstrip () for line in lines if line .strip ()] # just trim, keep everything
104116 # original quiet filtering
@@ -118,8 +130,21 @@ def is_successful(output: str) -> bool:
118130 has_failed = FAILED_MARKER in output
119131 return has_end and has_passed and not has_failed
120132
133+ def cleanup_godot_cache (quiet :bool = False , verbose :bool = False ):
134+ cache_dir = PROJECT_DIR / ".godot"
135+ if cache_dir .exists ():
136+ if not quiet :
137+ print ("→ Cleaning project cache" , end = " " )
138+ try :
139+ shutil .rmtree (cache_dir , ignore_errors = True )
140+ if not quiet :
141+ print ("[ DONE ]" )
142+ except Exception as e :
143+ if verbose :
144+ print (f"Warning: Failed to clean .godot: { e } " )
145+ elif not quiet :
146+ print ("[ FAILED ]" )
121147
122- def cleanup_godot_cache ():
123148 cache_dir = PROJECT_DIR / ".godot"
124149 if cache_dir .exists ():
125150 print (f"Cleaning project cache: { cache_dir } " )
@@ -128,8 +153,10 @@ def cleanup_godot_cache():
128153 except Exception as e :
129154 print (f"Warning: Failed to clean .godot: { e } " )
130155
131-
132- def run_godot (args : list [str ], desc : str , godot_bin : str , timeout_sec : int = TIMEOUT_SEC , verbose : bool = False ) -> tuple [int , str , bool ]:
156+ def run_godot (
157+ args : list [str ], desc : str , godot_bin : str , timeout_sec : int = TIMEOUT_SEC , quiet : bool = False ,
158+ verbose : bool = False , phase_code_on_fail = None
159+ ) -> tuple [int , str , bool ]:
133160 print (f"\n { '─' * 10 } { desc } { '─' * 10 } " )
134161 print (f"→ { godot_bin } { ' ' .join (args )} " )
135162
@@ -183,89 +210,135 @@ def run_godot(args: list[str], desc: str, godot_bin: str, timeout_sec: int = TIM
183210 print (msg )
184211 return 1 , msg , False
185212
186- def pre_import_project (godot_bin : str , verbose : bool ):
187- print ("\n Pre-importing project (headless, short timeout)..." )
188- cleanup_godot_cache ()
213+ def pre_import_project (godot_bin :str , quiet :bool = False , verbose :bool = False ):
214+ if verbose :
215+ print ("\n Pre-importing project (headless, short timeout)..." )
216+ elif not quiet : print ("→ Pre-import" , end = " " )
217+
218+ cleanup_godot_cache (quiet = quiet , verbose = verbose )
189219
190220 args = ["--path" , str (GODOT_PROJECT ), "--import" , "--headless" ]
191- exit_code , output , timed_out = run_godot (args , "Pre-import" , godot_bin , timeout_sec = IMPORT_TIMEOUT_SEC , verbose = verbose )
192-
221+ exit_code , output , timed_out = run_godot (
222+ args , "Pre-import" , godot_bin ,
223+ timeout_sec = IMPORT_TIMEOUT_SEC ,
224+ verbose = verbose , quiet = quiet
225+ )
193226 if timed_out or exit_code != 0 :
194227 if verbose :
195- print ("→ Pre-import failed or timed out (full output above)" )
196- else :
197- print ("→ Pre-import failed/crashed — continuing anyway" )
228+ print ("→ Pre-import failed or timed out — continuing anyway" )
229+ elif not quiet :
230+ print ("[ CRASH/FAIL ]" )
231+ return False , PHASE_PRE_IMPORT
198232 else :
199- print ("→ Pre-import completed" )
233+ if verbose :
234+ print ("→ Pre-import completed" )
235+ elif not quiet :
236+ print ("[ DONE ]" )
237+ return True , 0
200238
201239
202- def run_tests (mode : str , godot_bin : str , verbose ) -> bool :
240+ def run_tests (mode : str , godot_bin : str , quiet : bool = False , verbose : bool = False ) -> bool :
203241 overall_success = True
242+ failed_phase = 0
204243
205- # ── One-time preparation ──
206- print ("\n Preparing project (one-time cleanup + pre-import)..." )
207- cleanup_godot_cache () # Clean once at start
244+ if not quiet :
245+ print ("Preparing project..." )
246+ # ── One-time preparation ──
247+ if not quiet :
248+ print ("\n Preparing project (one-time cleanup + pre-import)..." )
249+ print ("Preparing project..." )
208250
209- pre_import_project (godot_bin , verbose ) # Attempt import once (ignore failures)
251+ ok , phase = pre_import_project (godot_bin , quiet = quiet , verbose = verbose ) # Attempt import once (ignore failures)
252+ if not ok :
253+ return False # early return — we'll set exit code higher up
210254
211255 # No more cleanups after this point — let the cache persist
212256
213257 if mode in ("unit" , "full" ):
258+ if not quiet : print ("→ Unit / headless tests" )
259+
214260 args = ["--path" , str (GODOT_PROJECT ), "--debug" , "--headless" , "--quit" ]
215- _ , output , timed_out = run_godot (args , "Unit / headless tests" , godot_bin , verbose = verbose )
261+ exit_code , output , timed_out = run_godot (
262+ args , "Unit / headless tests" , godot_bin ,
263+ verbose = verbose , quiet = quiet )
216264
217265 # Summary parsing still uses full output
218266 if timed_out :
219- print ("→ Unit phase: TIMEOUT" )
267+ if not quiet : print ("→ Unit phase: TIMEOUT" )
220268 overall_success = False
269+ failed_phase = PHASE_UNIT_TESTS
221270 elif not is_successful (output ):
222- print ("→ Unit phase: did NOT detect clean success" )
271+ if not quiet :
272+ print ("→ Unit phase: did NOT detect clean success" )
223273 overall_success = False
224- else :
225- print ("→ Unit phase: detected PASSED" )
274+ failed_phase = PHASE_UNIT_TESTS
275+ elif not quiet :
276+ # Show minimal success summary even in non-verbose
277+ lines = output .splitlines ()
278+ for line in lines :
279+ if any (m in line for m in [END_MARKER , PASSED_MARKER ]):
280+ print (line .strip ())
281+ print ("→ Unit phase: [ PASSED ]" )
226282
227- if not verbose :
228- # Optional: print a small reminder about the known error
229- if "ExampleInternal" in output :
230- print (" (known non-fatal warning about 'ExampleInternal' suppressed)" )
283+ if verbose :
284+ # Optional: print a small reminder about the known error
285+ if "ExampleInternal" in output :
286+ print (" (known non-fatal warning about 'ExampleInternal' suppressed)" )
231287
232288 return overall_success
233289
234-
235290# ──────────────────────────────────────────────
236291# Main
237292# ──────────────────────────────────────────────
238293def main ():
239294 parser = argparse .ArgumentParser (description = "Run godot-cpp test suite (temp portable Godot)" )
240295 parser .add_argument ("--unit-only" , action = "store_const" , const = "unit" , dest = "mode" )
241296 parser .add_argument ("--verbose" , action = "store_true" , default = False ,
242- help = "Show full unfiltered Godot output + more detailed runner messages" )
297+ help = "Show full unfiltered Godot output" )
298+ parser .add_argument ("--quiet" , action = "store_true" , default = False ,
299+ help = "Minimal output — only final exit code (for CI)" )
243300 args = parser .parse_args ()
244301
245- mode = args .mode or "full"
302+ mode = args .mode or "full"
246303 verbose = args .verbose
304+ quiet = args .quiet
247305
248- print (f"Original Godot: { ORIGINAL_GODOT } " )
249- print (f"Project: { GODOT_PROJECT } " )
250- print (f"Mode: { mode } " )
251- print (f"Verbose: { verbose } \n " )
306+ # ── Early exit for quiet mode if we want ultra-minimal ──
307+ if quiet :
308+ # We'll suppress almost all prints later
309+ def qprint (* a , ** kw ): pass
310+ global print
311+ print = qprint # monkey-patch print (crude but effective for this script)
252312
253- # Setup temp portable copy
254- godot_bin = setup_temp_portable_godot ()
313+ if quiet and verbose :
314+ print ("Error: --quiet and --verbose are mutually exclusive" , file = sys .stderr )
315+ sys .exit (1 )
316+
317+ print (f"Godot Executable: { ORIGINAL_GODOT } " )
318+ print (f"Project: { GODOT_PROJECT } " )
319+ print (f"Mode: { mode } " )
320+ print (f"Verbose: { verbose } \n " )
255321
322+ godot_bin = setup_temp_portable_godot (quiet = quiet )
323+
324+ exit_code = 0
256325 try :
257- all_passed = run_tests (mode , godot_bin , verbose )
326+ success = run_tests (mode , godot_bin , verbose = verbose , quiet = quiet )
327+ if not success :
328+ exit_code = 3 # default unit failure; overridden in run_tests if earlier phase
329+ except Exception as e :
330+ print (f"Runner crashed: { e } " , file = sys .stderr )
331+ exit_code = 1
258332 finally :
259- # Always cleanup temp files
260- cleanup_temp_portable ()
333+ cleanup_temp_portable (quiet = quiet , verbose = verbose )
261334
262- print ( " \n " + "═" * 40 )
263- if all_passed :
264- print ( "TEST SUITE PASSED")
265- sys . exit ( 0 )
266- else :
267- print ( "TEST SUITE FAILED" )
268- sys .exit (1 )
335+ if not quiet :
336+ print ( " \n " + "═" * 40 )
337+ status = " PASSED" if exit_code == 0 else f"FAILED (code { exit_code } )"
338+ duration = f" - took { int ( time . time () - start_time ) } s" if 'start_time' in globals () else ""
339+ print ( f"TEST SUITE { status } { duration } " )
340+
341+ sys .exit (exit_code )
269342
270343
271344if __name__ == "__main__" :
0 commit comments