2 # Copyright 2014 The Swarming Authors. All rights reserved.
3 # Use of this source code is governed under the Apache License, Version 2.0 that
4 # can be found in the LICENSE file.
24 TESTS_DIR = os.path.dirname(os.path.abspath(__file__))
25 ROOT_DIR = os.path.dirname(TESTS_DIR)
26 sys.path.insert(0, ROOT_DIR)
27 sys.path.insert(0, os.path.join(ROOT_DIR, 'third_party'))
29 from depot_tools import auto_stub
30 from utils import on_error
33 PEM = os.path.join(TESTS_DIR, 'self_signed.pem')
36 # Access to a protected member XXX of a client class - pylint: disable=W0212
41 (unicode(k), unicode(v.encode('ascii', 'replace')))
42 for k, v in os.environ.iteritems())
45 class HttpsServer(BaseHTTPServer.HTTPServer):
46 def __init__(self, addr, cls, hostname, pem):
47 BaseHTTPServer.HTTPServer.__init__(self, addr, cls)
48 self.hostname = hostname
50 self.socket = ssl.wrap_socket(
54 self.keep_running = True
60 return 'https://%s:%d' % (self.hostname, self.server_address[1])
63 assert not self._thread
66 while self.keep_running:
69 self._thread = threading.Thread(name='http', target=_server_loop)
70 self._thread.daemon = True
76 urllib.urlopen(self.url + '/_warmup').read()
82 self.keep_running = False
83 urllib.urlopen(self.url + '/_quit').read()
87 def register_call(self, request):
88 if request.path not in ('/_quit', '/_warmup'):
89 self.requests.append((request.path, request.parse_POST()))
92 class Handler(BaseHTTPServer.BaseHTTPRequestHandler):
93 def log_message(self, fmt, *args):
96 self.address_string(), self.log_date_time_string(), fmt % args)
99 ctype, pdict = cgi.parse_header(self.headers['Content-Type'])
100 if ctype == 'multipart/form-data':
101 return cgi.parse_multipart(self.rfile, pdict)
102 if ctype == 'application/x-www-form-urlencoded':
103 length = int(self.headers['Content-Length'])
104 return urlparse.parse_qs(self.rfile.read(length), keep_blank_values=1)
105 if ctype in ('application/json', 'application/json; charset=utf-8'):
106 length = int(self.headers['Content-Length'])
107 return json.loads(self.rfile.read(length))
111 self.server.register_call(self)
112 self.send_response(200)
113 self.send_header('Content-type', 'text/plain')
115 self.wfile.write('Rock on')
118 self.server.register_call(self)
119 self.send_response(200)
120 self.send_header('Content-type', 'application/json; charset=utf-8')
124 'url': 'https://localhost/error/1234',
126 self.wfile.write(json.dumps(data))
130 """Starts an HTTPS web server and returns the port bound."""
131 # A premade passwordless self-signed certificate. It works because urllib
132 # doesn't verify the certificate validity.
133 httpd = HttpsServer(('127.0.0.1', 0), Handler, 'localhost', pem=PEM)
138 class OnErrorBase(auto_stub.TestCase):
139 HOSTNAME = socket.getfqdn()
142 super(OnErrorBase, self).setUp()
145 self.mock(atexit, 'register', self._atexit.append)
146 self.mock(on_error, '_ENABLED_DOMAINS', (self.HOSTNAME,))
147 self.mock(on_error, '_HOSTNAME', None)
148 self.mock(on_error, '_SERVER', None)
149 self.mock(on_error, '_is_in_test', lambda: False)
152 class OnErrorTest(OnErrorBase):
153 def test_report(self):
154 url = 'https://localhost/'
155 on_error.report_on_exception_exit(url)
156 self.assertEqual([on_error._check_for_exception_on_exit], self._atexit)
157 self.assertEqual('https://localhost', on_error._SERVER.urlhost)
158 self.assertEqual(self.HOSTNAME, on_error._HOSTNAME)
159 with self.assertRaises(ValueError):
160 on_error.report_on_exception_exit(url)
162 def test_no_http(self):
163 # http:// url are denied.
164 url = 'http://localhost/'
165 self.assertIs(False, on_error.report_on_exception_exit(url))
166 self.assertEqual([], self._atexit)
169 class OnErrorServerTest(OnErrorBase):
170 def call(self, url, arg, returncode):
171 cmd = [sys.executable, 'on_error_test.py', 'run_shell_out', url, arg]
172 proc = subprocess.Popen(
173 cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=os.environ)
174 out = proc.communicate()[0]
175 logging.debug('\n%s', out)
176 self.assertEqual(returncode, proc.returncode)
179 def one_request(self, httpd):
180 self.assertEqual(1, len(httpd.requests))
181 resource, params = httpd.requests[0]
182 self.assertEqual('/ereporter2/api/v1/on_error', resource)
183 self.assertEqual(['r', 'v'], params.keys())
184 self.assertEqual('1', params['v'])
187 def test_shell_out_hacked(self):
188 # Rerun itself, report an error, ensure the error was reported.
189 httpd = start_server()
190 out = self.call(httpd.url, 'hacked', 0)
191 self.assertEqual([], httpd.requests)
192 self.assertEqual('', out)
195 def test_shell_out_report(self):
196 # Rerun itself, report an error manually, ensure the error was reported.
197 httpd = start_server()
198 out = self.call(httpd.url, 'report', 0)
200 'Sending the report ... done.\n'
201 'Report URL: https://localhost/error/1234\n'
203 self.assertEqual(expected, out)
205 actual = self.one_request(httpd)
206 self.assertGreater(actual.pop('duration'), 0.000001)
209 u'on_error_test.py', u'run_shell_out', unicode(httpd.url), u'report',
211 u'category': u'report',
212 u'cwd': unicode(os.getcwd()),
213 u'env': _serialize_env(),
214 u'hostname': unicode(socket.getfqdn()),
215 u'message': u'Oh dang',
216 u'os': unicode(sys.platform),
217 u'python_version': unicode(platform.python_version()),
218 u'source': u'on_error_test.py',
219 u'user': unicode(getpass.getuser()),
220 # The version was added dynamically for testing purpose.
223 self.assertEqual(expected, actual)
226 def test_shell_out_exception(self):
227 # Rerun itself, report an exception manually, ensure the error was reported.
228 httpd = start_server()
229 out = self.call(httpd.url, 'exception', 0)
231 'Sending the crash report ... done.\n'
232 'Report URL: https://localhost/error/1234\n'
233 'Really\nYou are not my type\n')
234 self.assertEqual(expected, out)
236 actual = self.one_request(httpd)
237 self.assertGreater(actual.pop('duration'), 0.000001)
238 # Remove numbers so editing the code doesn't invalidate the expectation.
239 actual['stack'] = re.sub(r' \d+', ' 0', actual['stack'])
242 u'on_error_test.py', u'run_shell_out', unicode(httpd.url), u'exception',
244 u'cwd': unicode(os.getcwd()),
245 u'category': u'exception',
246 u'env': _serialize_env(),
247 u'exception_type': u'TypeError',
248 u'hostname': unicode(socket.getfqdn()),
249 u'message': u'Really\nYou are not my type',
250 u'os': unicode(sys.platform),
251 u'python_version': unicode(platform.python_version()),
252 u'source': u'on_error_test.py',
254 u'File "on_error_test.py", line 0, in run_shell_out\n'
255 u' raise TypeError(\'You are not my type\')',
256 u'user': unicode(getpass.getuser()),
258 self.assertEqual(expected, actual)
261 def test_shell_out_exception_no_msg(self):
262 # Rerun itself, report an exception manually, ensure the error was reported.
263 httpd = start_server()
264 out = self.call(httpd.url, 'exception_no_msg', 0)
266 'Sending the crash report ... done.\n'
267 'Report URL: https://localhost/error/1234\n'
268 'You are not my type #2\n')
269 self.assertEqual(expected, out)
271 actual = self.one_request(httpd)
272 self.assertGreater(actual.pop('duration'), 0.000001)
273 # Remove numbers so editing the code doesn't invalidate the expectation.
274 actual['stack'] = re.sub(r' \d+', ' 0', actual['stack'])
277 u'on_error_test.py', u'run_shell_out', unicode(httpd.url),
280 u'category': u'exception',
281 u'cwd': unicode(os.getcwd()),
282 u'env': _serialize_env(),
283 u'exception_type': u'TypeError',
284 u'hostname': unicode(socket.getfqdn()),
285 u'message': u'You are not my type #2',
286 u'os': unicode(sys.platform),
287 u'python_version': unicode(platform.python_version()),
288 u'source': u'on_error_test.py',
290 u'File "on_error_test.py", line 0, in run_shell_out\n'
291 u' raise TypeError(\'You are not my type #2\')',
292 u'user': unicode(getpass.getuser()),
294 self.assertEqual(expected, actual)
297 def test_shell_out_crash(self):
298 # Rerun itself, report an error with a crash, ensure the error was reported.
299 httpd = start_server()
300 out = self.call(httpd.url, 'crash', 1)
302 'Traceback (most recent call last):\n'
303 ' File "on_error_test.py", line 0, in <module>\n'
304 ' sys.exit(run_shell_out(sys.argv[2], sys.argv[3]))\n'
305 ' File "on_error_test.py", line 0, in run_shell_out\n'
306 ' raise ValueError(\'Oops\')\n'
308 'Sending the crash report ... done.\n'
309 'Report URL: https://localhost/error/1234\n'
310 'Process exited due to exception\n'
312 # Remove numbers so editing the code doesn't invalidate the expectation.
313 self.assertEqual(expected, re.sub(r' \d+', ' 0', out))
315 actual = self.one_request(httpd)
316 # Remove numbers so editing the code doesn't invalidate the expectation.
317 actual['stack'] = re.sub(r' \d+', ' 0', actual['stack'])
318 self.assertGreater(actual.pop('duration'), 0.000001)
321 u'on_error_test.py', u'run_shell_out', unicode(httpd.url), u'crash',
323 u'category': u'exception',
324 u'cwd': unicode(os.getcwd()),
325 u'env': _serialize_env(),
326 u'exception_type': u'ValueError',
327 u'hostname': unicode(socket.getfqdn()),
328 u'message': u'Process exited due to exception\nOops',
329 u'os': unicode(sys.platform),
330 u'python_version': unicode(platform.python_version()),
331 u'source': u'on_error_test.py',
332 # The stack trace is stripped off the heading and absolute paths.
334 u'File "on_error_test.py", line 0, in <module>\n'
335 u' sys.exit(run_shell_out(sys.argv[2], sys.argv[3]))\n'
336 u'File "on_error_test.py", line 0, in run_shell_out\n'
337 u' raise ValueError(\'Oops\')',
338 u'user': unicode(getpass.getuser()),
340 self.assertEqual(expected, actual)
343 def test_shell_out_crash_server_down(self):
344 # Rerun itself, report an error, ensure the error was reported.
345 out = self.call('https://localhost:1', 'crash', 1)
347 'Traceback (most recent call last):\n'
348 ' File "on_error_test.py", line 0, in <module>\n'
349 ' sys.exit(run_shell_out(sys.argv[2], sys.argv[3]))\n'
350 ' File "on_error_test.py", line 0, in run_shell_out\n'
351 ' raise ValueError(\'Oops\')\n'
353 'Sending the crash report ... failed!\n'
354 'Process exited due to exception\n'
356 # Remove numbers so editing the code doesn't invalidate the expectation.
357 self.assertEqual(expected, re.sub(r' \d+', ' 0', out))
360 def run_shell_out(url, mode):
361 # Enable 'report_on_exception_exit' even though main file is *_test.py.
362 on_error._is_in_test = lambda: False
364 # Hack it out so registering works.
365 on_error._ENABLED_DOMAINS = (socket.getfqdn(),)
367 # Don't try to authenticate into localhost.
368 on_error.net.create_authenticator = lambda _: None
370 if not on_error.report_on_exception_exit(url):
371 print 'Failure to register the handler'
374 # Hack out certificate verification because we are using a self-signed
375 # certificate here. In practice, the SSL certificate is signed to guard
376 # against MITM attacks.
377 on_error._SERVER.engine.session.verify = False
380 # Sadly, net is a bit overly verbose, which breaks
381 # test_shell_out_crash_server_down.
382 logging.error = lambda *_, **_kwargs: None
383 logging.warning = lambda *_, **_kwargs: None
384 raise ValueError('Oops')
387 # Generate a manual report without an exception frame. Also set the version
389 setattr(sys.modules['__main__'], '__version__', '123')
390 on_error.report('Oh dang')
392 if mode == 'exception':
393 # Report from inside an exception frame.
395 raise TypeError('You are not my type')
397 on_error.report('Really')
399 if mode == 'exception_no_msg':
400 # Report from inside an exception frame.
402 raise TypeError('You are not my type #2')
404 on_error.report(None)
408 if __name__ == '__main__':
409 # Ignore _DISABLE_ENVVAR if set.
410 os.environ.pop(on_error._DISABLE_ENVVAR, None)
412 if len(sys.argv) == 4 and sys.argv[1] == 'run_shell_out':
413 sys.exit(run_shell_out(sys.argv[2], sys.argv[3]))
416 unittest.TestCase.maxDiff = None
418 level=logging.DEBUG if '-v' in sys.argv else logging.ERROR)