From cae11f94af815be823d6826a86f461b3032bd3a1 Mon Sep 17 00:00:00 2001 From: slipher Date: Thu, 9 Jul 2026 18:51:06 -0500 Subject: [PATCH 1/3] tools/crash_test.py: add --no-breakpad option Limits it to a rudimentary check that a crash dump is produced, without generating symbols or analyzing the dump. --- tools/crash_test.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tools/crash_test.py b/tools/crash_test.py index 636ec8505b..191dc77711 100755 --- a/tools/crash_test.py +++ b/tools/crash_test.py @@ -95,6 +95,8 @@ def Do(self): stderr=subprocess.PIPE, check=bool(self.tprefix)) dumps = os.listdir(PathJoin(self.dir, "crashdump")) assert len(dumps) == 1, dumps + if BREAKPAD_DIR is None: + return dump = PathJoin(self.dir, "crashdump", dumps[0]) sw_out = PathJoin(TEMP_DIR, self.name + "_stackwalk.log") with open(sw_out, "a+") as sw_f: @@ -149,7 +151,7 @@ def Do(self): engine = ModulePath(eng) assert os.path.isfile(engine), engine - if not SYMBOL_ZIPS: + if BREAKPAD_DIR is not None and not SYMBOL_ZIPS: target = ModulePath(module) assert os.path.isfile(target), target print(f"Symbolizing '{target}'...") @@ -173,6 +175,7 @@ def ArgParser(usage=None): " Otherwise, enter end-to-end mode: symbols are produced from the binaries and VM type defaults to 1 (NaCl from PWD). In this mode you will likely need to provide pak paths via --daemon-args.") ap.add_argument("--game-dir", type=str, default=".", help="Path to Daemon (+ gamelogic) binaries") ap.add_argument("--breakpad-dir", type=str, default=BREAKPAD_DIR, help=r"Path to Breakpad repo containing built dump_syms and stackwalk binaries. It may be a \\wsl.localhost\ path on Windows hosts in order to symbolize NaCl.") + ap.add_argument("--no-breakpad", action="store_true", help="Do not symbolize or stack-walk; only test for dump file existence") ap.add_argument("--give-up", action="store_true", help="Stop after first test failure") ap.add_argument("--nacl-arch", type=str, choices=["amd64", "i686", "armhf"], default="amd64") # TODO auto-detect? ap.add_argument("module", nargs="*", @@ -191,7 +194,7 @@ def ArgParser(usage=None): help="Extra arguments for Daemon (e.g. -pakpath)") pa = ap.parse_args(sys.argv[1:]) GAME_DIR = pa.game_dir -BREAKPAD_DIR = pa.breakpad_dir +BREAKPAD_DIR = None if pa.no_breakpad else pa.breakpad_dir GIVE_UP = pa.give_up DAEMON_USER_ARGS = pa.daemon_args NACL_ARCH = pa.nacl_arch @@ -207,9 +210,10 @@ def ArgParser(usage=None): if SYMBOL_ZIPS: print("Symbol zip(s) detected. Using release validation mode with pre-built symbols") - for z in SYMBOL_ZIPS: - with zipfile.ZipFile(PathJoin(GAME_DIR, z), 'r') as z: - z.extractall(SYMBOL_DIR) + if BREAKPAD_DIR: + for z in SYMBOL_ZIPS: + with zipfile.ZipFile(PathJoin(GAME_DIR, z), 'r') as z: + z.extractall(SYMBOL_DIR) else: print("No symbol zip detected. Using end-to-end Breakpad tooling test mode with dump_syms") From e085bffebd021da012c1cf333bf91ca76fef9e85 Mon Sep 17 00:00:00 2001 From: slipher Date: Thu, 4 Sep 2025 19:15:42 -0500 Subject: [PATCH 2/3] Miscellaneous tools/crash_test.py adjustments Each diff chunk here stands as a separate patch but let's avoid commit spam for a script no one cares about. - Increase NaCl timeout - Check existence of crash_server - Fix exception printing for Python < 3.10 - Fix interleaved stackwalk stdout and stderr (due to wrong truncate call) - Add Mac release support: there is no Breakpad, but the NaCl crash tests can run successfully (even through Rosetta!) --- tools/crash_test.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tools/crash_test.py b/tools/crash_test.py index 191dc77711..7683199e81 100755 --- a/tools/crash_test.py +++ b/tools/crash_test.py @@ -53,7 +53,7 @@ def Go(self): try: self.Do() except Exception as e: - traceback.print_exception(e) + traceback.print_exc() self.status = "FAILED" self.End() return self.status == "PASSED" @@ -73,12 +73,13 @@ def __init__(self, module, engine, tprefix, fault): def Do(self): vmtype = "0" if SYMBOL_ZIPS else "1" - print("Running daemon...") + print("Running Daemon...") p = subprocess.run([self.engine, "-noforward", # catch stupid Linux persistent sockets (if gamelogic test...) "-homepath", self.dir, "-set", "vm.sgame.type", vmtype, "-set", "vm.cgame.type", vmtype, + "-set", "vm.timeout", "30", # in case of emulator "-set", "sv_fps", "1000", "-set", "common.framerate.max", "0", "-set", "client.errorPopup", "0", @@ -101,8 +102,9 @@ def Do(self): sw_out = PathJoin(TEMP_DIR, self.name + "_stackwalk.log") with open(sw_out, "a+") as sw_f: print(f"Extracting stack trace to '{sw_out}'...") - sw_f.truncate() - subprocess.run(Virtualize([PathJoin(BREAKPAD_DIR, "src/processor/minidump_stackwalk"), dump, SYMBOL_DIR]), check=True, stdout=sw_f, stderr=subprocess.STDOUT) + sw_f.truncate(0) + subprocess.run(Virtualize([PathJoin(BREAKPAD_DIR, "src/processor/minidump_stackwalk"), dump, SYMBOL_DIR]), + check=True, stdout=sw_f, stderr=sw_f) sw_f.seek(0) sw = sw_f.read() TRACE_FUNC = "InjectFaultCmd::Run" @@ -148,6 +150,7 @@ def Do(self): else: tprefix = "" eng = module + assert os.path.isfile(PathJoin(GAME_DIR, "crash_server" + EXE)) engine = ModulePath(eng) assert os.path.isfile(engine), engine @@ -214,6 +217,10 @@ def ArgParser(usage=None): for z in SYMBOL_ZIPS: with zipfile.ZipFile(PathJoin(GAME_DIR, z), 'r') as z: z.extractall(SYMBOL_DIR) + if sys.platform == "darwin": + assert os.path.isdir(os.path.join(GAME_DIR, "Unvanquished.app")) + DAEMON_USER_ARGS = ["-pakpath", os.path.join(GAME_DIR, "pkg")] + DAEMON_USER_ARGS + GAME_DIR = os.path.join(GAME_DIR, "Unvanquished.app/Contents/MacOS") else: print("No symbol zip detected. Using end-to-end Breakpad tooling test mode with dump_syms") From d44eff24871eebe5ccdbcb9169e8b48b8ce9ff9c Mon Sep 17 00:00:00 2001 From: slipher Date: Wed, 29 Jul 2026 05:04:34 -0500 Subject: [PATCH 3/3] crash_test.py: require function OR file names Report missing function and/or file names from Breakpad stack walk but pass the test if just 1 is present. --- tools/crash_test.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/tools/crash_test.py b/tools/crash_test.py index 7683199e81..a9e899610c 100755 --- a/tools/crash_test.py +++ b/tools/crash_test.py @@ -40,13 +40,16 @@ def Begin(self): def End(self): print(f"==={self.status}: {self.name}===") - def Verify(self, cond, reason=None): + def Verify(self, cond, reason=None, fail=True): if not cond: - if reason is not None: - print(f"FAILURE: {reason}") - self.status = "FAILED" - if GIVE_UP: - raise Exception("Giving up on first failure") + if fail: + if reason is not None: + print(f"FAILURE: {reason}") + self.status = "FAILED" + if GIVE_UP: + raise Exception("Giving up on first failure") + else: + print(f"WARNING: {reason}") def Go(self): self.Begin() @@ -107,8 +110,10 @@ def Do(self): check=True, stdout=sw_f, stderr=sw_f) sw_f.seek(0) sw = sw_f.read() - TRACE_FUNC = "InjectFaultCmd::Run" - self.Verify(TRACE_FUNC in sw, "function names not found in trace (did you build with symbols?)") + # Check both function names and filenames. On Linux it seems like only one of them works at a time?? + self.Verify(srcnames := ("Command.cpp" in sw), "source file names not found in trace", fail=False) + self.Verify(funcnames := ("InjectFaultCmd::Run" in sw), "function names not found in trace", fail=False) + self.Verify(srcnames or funcnames, "neither file nor function names found in trace") def Virtualize(cmdline): bin, *args = cmdline