Skip to content

Commit a20bdd9

Browse files
committed
windows
1 parent 40b858d commit a20bdd9

File tree

1 file changed

+127
-77
lines changed

1 file changed

+127
-77
lines changed

tools/crash_test.py

Lines changed: 127 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,12 @@
11
#!/usr/bin/env python3
22

3+
import argparse
34
import os
5+
import shutil
46
import subprocess
57
import sys
68
import 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-
3010
class 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

Comments
 (0)