11#!/usr/bin/env python3
22
3+ import argparse
34import os
5+ import shutil
46import subprocess
57import sys
68import traceback
79
8- DAEMON_DIR = '/unv/Unvanquished/daemon'
9- BREAKPAD_DIR = '/unv/Unvanquished/daemon/libs/breakpad'
10- GAME_BUILD_DIR = '.'
11- ARCH = 'amd64'
12-
13- SYMBOLIZE = os .path .join (BREAKPAD_DIR , "symbolize.py" )
14- STACKWALK = os .path .join (BREAKPAD_DIR , "src/processor/minidump_stackwalk" )
15-
16- DAEMON_TTYCLIENT = os .path .join (GAME_BUILD_DIR , 'daemon-tty' )
17- DAEMON_SERVER = os .path .join (GAME_BUILD_DIR , 'daemonded' )
18- TEMP_DIR = os .path .join (GAME_BUILD_DIR , "crashtest-tmp" )
19- os .makedirs (TEMP_DIR , exist_ok = True )
20- SYMBOL_DIR = os .path .join (TEMP_DIR , "symbols" )
21- os .makedirs (SYMBOL_DIR , exist_ok = True )
22- HOMEPATH = os .path .join (TEMP_DIR , "homepath" )
23-
24-
25-
26- assert os .path .isfile (DAEMON_SERVER )
27- assert os .path .isfile (STACKWALK )
28-
29-
3010class CrashTest :
3111 def __init__ (self , name ):
3212 self .name = name
@@ -38,10 +18,13 @@ def Begin(self):
3818 def End (self ):
3919 print (f"==={ self .status } : { self .name } ===" )
4020
41- def Verify (self , cond , reason ):
21+ def Verify (self , cond , reason = None ):
4222 if not cond :
43- print (f"FAILURE: { reason } " )
23+ if reason is not None :
24+ print (f"FAILURE: { reason } " )
4425 self .status = "FAILED"
26+ if GIVE_UP :
27+ raise Exception ("Giving up on first failure" )
4528
4629 def Go (self ):
4730 self .Begin ()
@@ -53,80 +36,147 @@ def Go(self):
5336 self .End ()
5437 return self .status == "PASSED"
5538
56- class NaclCrashTest (CrashTest ):
57- def __init__ (self , engine , tprefix , fault ):
58- super ().__init__ (f"nacl. { fault } " )
39+ class BreakpadCrashTest (CrashTest ):
40+ def __init__ (self , module , engine , tprefix , fault ):
41+ super ().__init__ (module + "." + fault )
5942 self .engine = engine
6043 self .tprefix = tprefix
6144 self .fault = fault
45+ self .dir = os .path .join (TEMP_DIR , self .name )
46+ try :
47+ shutil .rmtree (self .dir )
48+ except FileNotFoundError :
49+ pass
50+ os .makedirs (self .dir )
6251
6352 def Do (self ):
6453 print ("Running daemon..." )
65- p = subprocess .run ([self .engine , "-set" , "vm.sgame.type" , "1" ,
54+ p = subprocess .run ([self .engine ,
55+ "-set" , "vm.sgame.type" , "1" ,
6656 "-set" , "vm.cgame.type" , "1" ,
67- #"-set", "logs.level.fs", "warning",
68- "-set" , "sv_fps" , "1000" ,
69- "-set" , "common.framerate.max" , "0" ,
70- #"-set", "server.private", "2",
71- #"-set", "sv_networkScope", "0",
72- "-set" , "net_enabled" , "0" ,
73- "-set" , "common.framerate.max" , "0" ,
74- "-pakpath" , "/unv/Unvanquished/pkg" ,
75- #"-homepath", HOMEPATH,
76- #"-pakpath", os.path.join(DAEMON_DIR, "pkg"),
77- #"-set", "fs_basepak", "testdata",
78- "+devmap plat23" ,
79- "+delay 20f echo CRASHTEST_BEGIN" ,
80- f"+delay 20f { self .tprefix } injectFault" , self .fault ,
81- "+delay 20f echo CRASHTEST_END" ,
82- "+delay 40f quit" ],
83- stderr = subprocess .PIPE , check = False )
84- log = [s .strip () for s in p .stderr .decode ("utf8" ).splitlines ()]
85- i = log .index ("CRASHTEST_BEGIN" )
86- #j = log.index("CRASHTEST_END")
87- j = len (log ) - 1
88- # TODO expected vs. actual Warn's
89- DUMP_PREFIX = "Wrote crash dump to "
90- dumps = [l for l in log if l .startswith (DUMP_PREFIX )]
91- assert len (dumps ) == 1 , "Daemon log contains 1 crash dump"
92- dump = dumps [0 ][len (DUMP_PREFIX ):]
93- sw_out = os .path .join (TEMP_DIR , self .tprefix + self .fault + ".stackwalk.log" )
57+ "-set" , "sv_fps" , "1000" ,
58+ "-set" , "common.framerate.max" , "0" ,
59+ "-set" , "client.errorPopup" , "0" ,
60+ "-set" , "server.private" , "2" ,
61+ "-set" , "net_enabled" , "0" ,
62+ "-set" , "common.framerate.max" , "0" ,
63+ "-set" , "cg_navgenOnLoad" , "0" ,
64+ "-homepath" , self .dir ,
65+ * DAEMON_USER_ARGS ,
66+ * ["+devmap plat23" ] * (self .tprefix == "sgame." ),
67+ "+delay 20f echo CRASHTEST_BEGIN" ,
68+ f"+delay 20f { self .tprefix } injectFault" , self .fault ,
69+ "+delay 20f echo CRASHTEST_END" ,
70+ "+delay 40f quit" ],
71+ stderr = subprocess .PIPE , check = bool (self .tprefix ))
72+ dumps = os .listdir (os .path .join (self .dir , "crashdump" ))
73+ assert len (dumps ) == 1 , dumps
74+ dump = os .path .join (self .dir , "crashdump" , dumps [0 ])
75+ sw_out = os .path .join (TEMP_DIR , self .name + "_stackwalk.log" )
9476 with open (sw_out , "a+" ) as sw_f :
9577 print (f"Extracting stack trace to '{ sw_out } '..." )
9678 sw_f .truncate ()
97- subprocess .run ([ STACKWALK , dump , SYMBOL_DIR ], check = True , stdout = sw_f , stderr = subprocess .STDOUT )
79+ subprocess .run (Virtualize ([ os . path . join ( BREAKPAD_DIR , "src/processor/minidump_stackwalk" ), dump , SYMBOL_DIR ]) , check = True , stdout = sw_f , stderr = subprocess .STDOUT )
9880 sw_f .seek (0 )
9981 sw = sw_f .read ()
10082 TRACE_FUNC = "InjectFaultCmd::Run"
10183 self .Verify (TRACE_FUNC in sw , "function names not found in trace (did you build with symbols?)" )
10284
103- def DoModule (module ):
104- try :
105- engine = DAEMON_TTYCLIENT
85+ def Virtualize (cmdline ):
86+ bin , * args = cmdline
87+ if EXE :
88+ bin2 = bin .replace ("\\ " , "/" )
89+ if bin2 .startswith ("//wsl.localhost/" ):
90+ parts = bin2 .split ("/" )
91+ vm = parts [3 ]
92+ path = "/" + "/" .join (parts [4 :])
93+ return ["wsl" , "-d" , vm , "--" , path ] + args
94+ if bin .endswith (".py" ):
95+ return [sys .executable ] + cmdline
96+ return cmdline
97+
98+ def ModulePath (module ):
99+ base = {
100+ "dummyapp" : "dummyapp" + EXE ,
101+ "server" : "daemonded" + EXE ,
102+ "ttyclient" : "daemon-tty" + EXE ,
103+ "client" : "daemon" + EXE ,
104+ "sgame" : f"sgame-{ NACL_ARCH } .nexe" ,
105+ "cgame" : f"cgame-{ NACL_ARCH } .nexe" ,
106+ }[module ]
107+ return os .path .join (GAME_BUILD_DIR , base )
108+
109+ class ModuleCrashTests (CrashTest ):
110+ def __init__ (self , module , engine = None ):
111+ super ().__init__ (module )
112+ self .engine = engine
113+
114+ def Do (self ):
115+ module = self .name
106116 if module == "sgame" :
107- target = os . path . join ( GAME_BUILD_DIR , f"sgame- { ARCH } .nexe" )
117+ eng = self . engine or "server"
108118 tprefix = "sgame."
109119 elif module == "cgame" :
110- target = os .path .join (GAME_BUILD_DIR , f"cgame-{ ARCH } .nexe" )
111120 tprefix = "cgame."
112- elif module == "server" :
121+ eng = self .engine or "ttyclient"
122+ else :
113123 tprefix = ""
114- engine = target = DAEMON_SERVER
124+ eng = module
125+ engine = ModulePath (eng )
126+ target = ModulePath (module )
115127
116128 assert os .path .isfile (target ), target
129+ assert os .path .isfile (engine ), engine
117130 print (f"Symbolizing '{ target } '..." )
118- subprocess .check_call ([sys .executable , SYMBOLIZE , "--symbol-directory" , SYMBOL_DIR , target ])
119-
120- return (True
121- & NaclCrashTest (DAEMON_TTYCLIENT , tprefix , "exception" ).Go ()
122- & NaclCrashTest (DAEMON_TTYCLIENT , tprefix , "throw" ).Go ()
123- & NaclCrashTest (DAEMON_TTYCLIENT , tprefix , "abort" ).Go ()
124- & NaclCrashTest (DAEMON_TTYCLIENT , tprefix , "segfault" ).Go ())
125- except Exception :
126- raise
127- return False
128-
129- #passed = DoModule("sgame") & DoModule("cgame")
130- passed = DoModule ("server" )
131- sys .exit (1 - passed )
131+ subprocess .check_call (Virtualize ([os .path .join (BREAKPAD_DIR , "symbolize.py" ),
132+ "--symbol-directory" , SYMBOL_DIR , target ]))
133+
134+ self .Verify (BreakpadCrashTest (module , engine , tprefix , "segfault" ).Go ())
135+ if tprefix or not EXE :
136+ # apparently abort() is caught on Linux but not Windows
137+ self .Verify (BreakpadCrashTest (module , engine , tprefix , "abort" ).Go ())
138+ if tprefix :
139+ self .Verify (BreakpadCrashTest (module , engine , tprefix , "exception" ).Go ())
140+ self .Verify (BreakpadCrashTest (module , engine , tprefix , "throw" ).Go ())
141+
142+ def ArgParser (usage = None ):
143+ ap = argparse .ArgumentParser (usage = usage )
144+ 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." )
145+ ap .add_argument ("--give-up" , action = "store_true" , help = "Stop after first test failure" )
146+ ap .add_argument ("--nacl-arch" , type = str , choices = ["amd64" , "i686" , "armhf" ], default = "amd64" ) # TODO auto-detect?
147+ ap .add_argument ("module" , nargs = "*" , choices = [
148+ "dummyapp" , "server" , "ttyclient" , "client" ,
149+ "cgame" , "ttyclient:cgame" , "client:cgame" ,
150+ "sgame" , "server:sgame" , "ttyclient:sgame" , "client:sgame" ])
151+ return ap
152+
153+ BREAKPAD_DIR = os .path .abspath (os .path .join (
154+ os .path .dirname (os .path .realpath (__file__ )), "../libs/breakpad" ))
155+ GAME_BUILD_DIR = '.' # WSL calls rely on relative paths
156+ TEMP_DIR = os .path .join (GAME_BUILD_DIR , "crashtest-tmp" )
157+ SYMBOL_DIR = os .path .join (TEMP_DIR , "symbols" )
132158
159+ os .makedirs (TEMP_DIR , exist_ok = True )
160+ os .makedirs (SYMBOL_DIR , exist_ok = True )
161+
162+ if os .name == "nt" :
163+ EXE = '.exe'
164+ else :
165+ EXE = ""
166+
167+ ap = ArgParser (usage = ArgParser ().format_usage ().rstrip ().removeprefix ("usage: " )
168+ + " [--daemon-args ARGS...]" )
169+ ap .add_argument ("--daemon-args" , nargs = argparse .REMAINDER , default = [],
170+ help = "Extra arguments for Daemon (e.g. -pakpath)" )
171+ pa = ap .parse_args (sys .argv [1 :])
172+ BREAKPAD_DIR = pa .breakpad_dir
173+ GIVE_UP = pa .give_up
174+ DAEMON_USER_ARGS = pa .daemon_args
175+ NACL_ARCH = pa .nacl_arch
176+ modules = pa .module or ["server" , "ttyclient" , "sgame" , "cgame" ]
177+ passed = True
178+ for module in modules :
179+ passed &= ModuleCrashTests (* module .split (":" )[::- 1 ]).Go ()
180+ if not passed and GIVE_UP :
181+ break
182+ sys .exit (1 - passed )
0 commit comments