Skip to content

Commit 9465d67

Browse files
committed
[test] add case for crash dump uploading
1 parent 8c7fd9c commit 9465d67

File tree

6 files changed

+411
-0
lines changed

6 files changed

+411
-0
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
6+
<title>test package</title>
7+
</head>
8+
<body>
9+
<button onclick="nw.App.crashBrowser()">crash browser</button>
10+
<button onclick="nw.App.crashRenderer()">crash renderer</button>
11+
<script>
12+
if (nw.App.argv.indexOf("--test-renderer-crash") > -1)
13+
nw.App.crashRenderer();
14+
else
15+
nw.App.crashBrowser();
16+
17+
</script>
18+
</body>
19+
</html>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "crash-report-test",
3+
"crash_report_url": "http://localhost:2345/reportloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongurl",
4+
"main": "index.html"
5+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/usr/bin/env python
2+
# Copyright 2016 The Chromium Authors. All rights reserved.
3+
# Use of this source code is governed by a BSD-style license that can be
4+
# found in the LICENSE file.
5+
6+
"""Exceptions used by the Kasko integration test module."""
7+
8+
import BaseHTTPServer
9+
import cgi
10+
import logging
11+
import os
12+
import socket
13+
import threading
14+
import time
15+
import uuid
16+
import json
17+
import gzip
18+
from StringIO import StringIO
19+
20+
_LOGGER = logging.getLogger(os.path.basename(__file__))
21+
22+
23+
class _StoppableHTTPServer(BaseHTTPServer.HTTPServer):
24+
"""An extension of BaseHTTPServer that uses timeouts and is interruptable."""
25+
26+
def server_bind(self):
27+
BaseHTTPServer.HTTPServer.server_bind(self)
28+
self.socket.settimeout(1)
29+
self.run_ = True
30+
31+
def get_request(self):
32+
while self.run_:
33+
try:
34+
sock, addr = self.socket.accept()
35+
sock.settimeout(None)
36+
return (sock, addr)
37+
except socket.timeout:
38+
pass
39+
40+
def stop(self):
41+
self.run_ = False
42+
43+
def serve(self):
44+
while self.run_:
45+
self.handle_request()
46+
47+
48+
class CrashServer(object):
49+
"""A simple crash server for testing."""
50+
51+
def __init__(self):
52+
self.server_ = None
53+
self.lock_ = threading.Lock()
54+
self.crashes_ = [] # Under lock_.
55+
56+
def crash(self, index):
57+
"""Accessor for the list of crashes."""
58+
with self.lock_:
59+
if index >= len(self.crashes_):
60+
return None
61+
return self.crashes_[index]
62+
63+
@property
64+
def port(self):
65+
"""Returns the port associated with the server."""
66+
if not self.server_:
67+
return 0
68+
return self.server_.server_port
69+
70+
def start(self):
71+
"""Starts the server on another thread. Call from main thread only."""
72+
page_handler = self.multipart_form_handler()
73+
self.server_ = _StoppableHTTPServer(('127.0.0.1', 0), page_handler)
74+
self.thread_ = self.server_thread()
75+
self.thread_.start()
76+
77+
def stop(self):
78+
"""Stops the running server. Call from main thread only."""
79+
self.server_.stop()
80+
self.thread_.join()
81+
self.server_ = None
82+
self.thread_ = None
83+
84+
def wait_for_report(self, timeout):
85+
"""Waits until the server has received a crash report.
86+
87+
Returns True if the a report has been received in the given time, or False
88+
if a timeout occurred. Since Python condition variables have no notion of
89+
timeout this is, sadly, a busy loop on the calling thread.
90+
"""
91+
started = time.time()
92+
elapsed = 0
93+
while elapsed < timeout:
94+
with self.lock_:
95+
if len(self.crashes_):
96+
return True
97+
time.sleep(0.1)
98+
elapsed = time.time() - started
99+
100+
return False
101+
102+
103+
def multipart_form_handler(crash_server):
104+
"""Returns a multi-part form handler class for use with a BaseHTTPServer."""
105+
106+
class MultipartFormHandler(BaseHTTPServer.BaseHTTPRequestHandler):
107+
"""A multi-part form handler that processes crash reports.
108+
109+
This class only handles multipart form POST messages, with all other
110+
requests by default returning a '501 not implemented' error.
111+
"""
112+
113+
def __init__(self, request, client_address, socket_server):
114+
BaseHTTPServer.BaseHTTPRequestHandler.__init__(
115+
self, request, client_address, socket_server)
116+
117+
def log_message(self, format, *args):
118+
_LOGGER.debug(format, *args)
119+
120+
def do_POST(self):
121+
"""Handles POST messages contained multipart form data."""
122+
content_type, parameters = cgi.parse_header(
123+
self.headers.getheader('content-type'))
124+
if content_type != 'multipart/form-data':
125+
raise Exception('Unsupported Content-Type: ' + content_type)
126+
if self.headers.getheader('content-encoding') == 'gzip':
127+
self.log_message('GZIP');
128+
readsize = self.headers.getheader('content-length')
129+
buffer = StringIO(self.rfile.read(int(readsize)))
130+
with gzip.GzipFile(fileobj=buffer, mode="r") as f:
131+
post_multipart = cgi.parse_multipart(f, parameters)
132+
else:
133+
post_multipart = cgi.parse_multipart(self.rfile, parameters)
134+
self.log_message("got part")
135+
136+
# Save the crash report.
137+
report = dict(post_multipart.items())
138+
report_id = str(uuid.uuid4())
139+
report['report-id'] = [report_id]
140+
self.log_message("got report %s", report_id)
141+
self.log_message("%s", json.dumps(report.keys()))
142+
with crash_server.lock_:
143+
crash_server.crashes_.append(report)
144+
145+
# Send the response.
146+
self.send_response(200)
147+
self.send_header("Content-Type", "text/plain")
148+
self.end_headers()
149+
self.wfile.write(report_id)
150+
151+
return MultipartFormHandler
152+
153+
def server_thread(crash_server):
154+
"""Returns a thread that hosts the webserver."""
155+
156+
class ServerThread(threading.Thread):
157+
def run(self):
158+
crash_server.server_.serve()
159+
160+
return ServerThread()
161+
162+
def main():
163+
server = CrashServer()
164+
server.start()
165+
166+
if __name__ == "__main__":
167+
main()
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env python
2+
# Copyright 2016 The Chromium Authors. All rights reserved.
3+
# Use of this source code is governed by a BSD-style license that can be
4+
# found in the LICENSE file.
5+
6+
"""Utility functions for dealing with crash reports."""
7+
8+
import logging
9+
import os
10+
_LOGGER = logging.getLogger(os.path.basename(__file__))
11+
12+
13+
def LogCrashKeys(report):
14+
for key in sorted(report.keys()):
15+
val = report[key][0]
16+
if (len(val) < 64):
17+
_LOGGER.debug('Got crashkey "%s": "%s"', key, val)
18+
else:
19+
_LOGGER.debug('Got crashkey "%s": ...%d bytes...', key, len(val))
20+
21+
22+
def ValidateCrashReport(report, expectations=None):
23+
expected_keys = {}
24+
25+
# Merge in additional expectations.
26+
if expectations:
27+
for key, value in expectations.iteritems():
28+
expected_keys[key] = value
29+
30+
# Validate the expectations.
31+
missing_keys = False
32+
for expected_key, error in expected_keys.iteritems():
33+
if expected_key not in report:
34+
_LOGGER.error('Missing expected "%s" crash key.', expected_key)
35+
_LOGGER.error('"%s" integration appears broken.', error)
36+
missing_keys = True
37+
38+
if missing_keys:
39+
raise Exception('Missing expected crash keys.')

test/sanity/crash-dump-report/test.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import time
2+
import os
3+
import shutil
4+
import zipfile
5+
import platform
6+
import subprocess
7+
import sys
8+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
9+
10+
import logging
11+
logging.basicConfig(level=logging.DEBUG)
12+
13+
import crash_server
14+
import nw_util
15+
import report
16+
17+
18+
_LOGGER = logging.getLogger(os.path.basename(__file__))
19+
20+
testdir = os.path.dirname(os.path.abspath(__file__))
21+
nwdist = os.path.join(os.path.dirname(os.environ['CHROMEDRIVER']), 'nwdist')
22+
23+
appdir = os.path.join(testdir, 'app')
24+
pkg1 = os.path.join(testdir, 'pkg1')
25+
26+
try:
27+
shutil.rmtree(pkg1)
28+
except:
29+
pass
30+
31+
def copytree(src, dst, symlinks=False, ignore=None):
32+
if not os.path.exists(dst):
33+
os.makedirs(dst)
34+
for item in os.listdir(src):
35+
s = os.path.join(src, item)
36+
d = os.path.join(dst, item)
37+
if os.path.isdir(s):
38+
copytree(s, d, symlinks, ignore)
39+
else:
40+
if not os.path.exists(d) or os.stat(s).st_mtime - os.stat(d).st_mtime > 1:
41+
shutil.copy2(s, d)
42+
43+
# create test directory
44+
os.mkdir(pkg1)
45+
46+
# copy nw to test directory
47+
print "copying %s to %s" % (nwdist, pkg1)
48+
copytree(nwdist, pkg1)
49+
pkgname = 'crash-report-test'
50+
51+
# copy app to test directory
52+
if platform.system() == 'Darwin':
53+
appdest = os.path.join(pkg1, 'nwjs.app', 'Contents', 'Resources', 'app.nw')
54+
user_data_dir = os.path.join(os.getenv('HOME'), 'Library', 'Application Support', pkgname)
55+
exe = os.path.join(pkg1, 'nwjs.app', 'Contents', 'MacOS', 'nwjs')
56+
check_file = os.path.join(user_data_dir, 'Default', 'Web Data')
57+
elif platform.system() == 'Linux':
58+
appdest = pkg1
59+
user_data_dir = os.path.join(os.getenv('HOME'), '.config', pkgname)
60+
exe = os.path.join(pkg1, 'nw')
61+
check_file = os.path.join(user_data_dir, 'Default', 'Web Data')
62+
else:
63+
appdest = pkg1
64+
user_data_dir = os.path.join(os.getenv('LOCALAPPDATA'), pkgname)
65+
check_file = os.path.join(user_data_dir, 'User Data', 'Default', 'Web Data')
66+
exe = os.path.join(pkg1, 'nw.exe')
67+
68+
print "copying %s to %s" % (appdir, appdest)
69+
copytree(appdir, appdest)
70+
71+
print "user data dir: %s" % (user_data_dir)
72+
73+
os.chdir(pkg1)
74+
75+
for arg in ['--test-browser-crash', '--test-renderer-crash']:
76+
try:
77+
shutil.rmtree(user_data_dir)
78+
except:
79+
pass
80+
server = crash_server.CrashServer()
81+
with nw_util.ScopedStartStop(server):
82+
_LOGGER.info('Started server on port %d', server.port)
83+
manifest = open(os.path.join(appdest, 'package.json'), 'w')
84+
manifest.write('''
85+
{
86+
"name":"%s",
87+
"version": "0.0.12",
88+
"crash_report_url": "http://localhost:%d/reportloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongurl",
89+
"main": "index.html"
90+
}
91+
''' % (pkgname, server.port))
92+
manifest.close()
93+
94+
assert not os.path.exists(user_data_dir), "'%s' should not be existed before testing" % user_data_dir
95+
96+
p = subprocess.Popen([exe, arg])
97+
_LOGGER.info('Waiting for crash report')
98+
try:
99+
if not server.wait_for_report(10):
100+
raise Exception('No report received.')
101+
finally:
102+
p.terminate()
103+
r = server.crash(0)
104+
report.LogCrashKeys(r)
105+
ptype = r['ptype'][0]
106+
assert(ptype == 'browser' and arg == '--test-browser-crash' or ptype in ['renderer', 'extension'] and arg == '--test-renderer-crash')
107+
assert(pkgname == r['prod'][0])
108+
assert(r['ver'][0] == "0.0.12")
109+
assert(len(r['upload_file_minidump'][0]) > 100000)
110+

0 commit comments

Comments
 (0)