Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / tools / swarming_client / tests / on_error_test.py
1 #!/usr/bin/env python
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.
5
6 import BaseHTTPServer
7 import atexit
8 import cgi
9 import getpass
10 import json
11 import logging
12 import os
13 import platform
14 import re
15 import socket
16 import ssl
17 import subprocess
18 import sys
19 import threading
20 import unittest
21 import urllib
22 import urlparse
23
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'))
28
29 from depot_tools import auto_stub
30 from utils import on_error
31
32
33 PEM = os.path.join(TESTS_DIR, 'self_signed.pem')
34
35
36 # Access to a protected member XXX of a client class - pylint: disable=W0212
37
38
39 def _serialize_env():
40   return dict(
41       (unicode(k), unicode(v.encode('ascii', 'replace')))
42       for k, v in os.environ.iteritems())
43
44
45 class HttpsServer(BaseHTTPServer.HTTPServer):
46   def __init__(self, addr, cls, hostname, pem):
47     BaseHTTPServer.HTTPServer.__init__(self, addr, cls)
48     self.hostname = hostname
49     self.pem = pem
50     self.socket = ssl.wrap_socket(
51         self.socket,
52         server_side=True,
53         certfile=self.pem)
54     self.keep_running = True
55     self.requests = []
56     self._thread = None
57
58   @property
59   def url(self):
60     return 'https://%s:%d' % (self.hostname, self.server_address[1])
61
62   def start(self):
63     assert not self._thread
64
65     def _server_loop():
66       while self.keep_running:
67         self.handle_request()
68
69     self._thread = threading.Thread(name='http', target=_server_loop)
70     self._thread.daemon = True
71     self._thread.start()
72
73     while True:
74       # Ensures it is up.
75       try:
76         urllib.urlopen(self.url + '/_warmup').read()
77       except IOError:
78         continue
79       return
80
81   def stop(self):
82     self.keep_running = False
83     urllib.urlopen(self.url + '/_quit').read()
84     self._thread.join()
85     self._thread = None
86
87   def register_call(self, request):
88     if request.path not in ('/_quit', '/_warmup'):
89       self.requests.append((request.path, request.parse_POST()))
90
91
92 class Handler(BaseHTTPServer.BaseHTTPRequestHandler):
93   def log_message(self, fmt, *args):
94     logging.debug(
95         '%s - - [%s] %s',
96         self.address_string(), self.log_date_time_string(), fmt % args)
97
98   def parse_POST(self):
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))
108     assert False, ctype
109
110   def do_GET(self):
111     self.server.register_call(self)
112     self.send_response(200)
113     self.send_header('Content-type', 'text/plain')
114     self.end_headers()
115     self.wfile.write('Rock on')
116
117   def do_POST(self):
118     self.server.register_call(self)
119     self.send_response(200)
120     self.send_header('Content-type', 'application/json; charset=utf-8')
121     self.end_headers()
122     data = {
123       'id': '1234',
124       'url': 'https://localhost/error/1234',
125     }
126     self.wfile.write(json.dumps(data))
127
128
129 def start_server():
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)
134   httpd.start()
135   return httpd
136
137
138 class OnErrorBase(auto_stub.TestCase):
139   HOSTNAME = socket.getfqdn()
140
141   def setUp(self):
142     super(OnErrorBase, self).setUp()
143     os.chdir(TESTS_DIR)
144     self._atexit = []
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)
150
151
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)
161
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)
167
168
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)
177     return out
178
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'])
185     return params['r']
186
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)
193     httpd.stop()
194
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)
199     expected = (
200         'Sending the report ... done.\n'
201         'Report URL: https://localhost/error/1234\n'
202         'Oh dang\n')
203     self.assertEqual(expected, out)
204
205     actual = self.one_request(httpd)
206     self.assertGreater(actual.pop('duration'), 0.000001)
207     expected = {
208       u'args': [
209         u'on_error_test.py', u'run_shell_out', unicode(httpd.url), u'report',
210       ],
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.
221       u'version': u'123',
222     }
223     self.assertEqual(expected, actual)
224     httpd.stop()
225
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)
230     expected = (
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)
235
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'])
240     expected = {
241       u'args': [
242         u'on_error_test.py', u'run_shell_out', unicode(httpd.url), u'exception',
243       ],
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',
253       u'stack':
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()),
257     }
258     self.assertEqual(expected, actual)
259     httpd.stop()
260
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)
265     expected = (
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)
270
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'])
275     expected = {
276       u'args': [
277         u'on_error_test.py', u'run_shell_out', unicode(httpd.url),
278         u'exception_no_msg',
279       ],
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',
289       u'stack':
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()),
293     }
294     self.assertEqual(expected, actual)
295     httpd.stop()
296
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)
301     expected = (
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'
307         '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'
311         'Oops\n')
312     # Remove numbers so editing the code doesn't invalidate the expectation.
313     self.assertEqual(expected, re.sub(r' \d+', ' 0', out))
314
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)
319     expected = {
320       u'args': [
321         u'on_error_test.py', u'run_shell_out', unicode(httpd.url), u'crash',
322       ],
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.
333       u'stack':
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()),
339     }
340     self.assertEqual(expected, actual)
341     httpd.stop()
342
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)
346     expected = (
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'
352         'ValueError: Oops\n'
353         'Sending the crash report ... failed!\n'
354         'Process exited due to exception\n'
355         'Oops\n')
356     # Remove numbers so editing the code doesn't invalidate the expectation.
357     self.assertEqual(expected, re.sub(r' \d+', ' 0', out))
358
359
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
363
364   # Hack it out so registering works.
365   on_error._ENABLED_DOMAINS = (socket.getfqdn(),)
366
367   # Don't try to authenticate into localhost.
368   on_error.net.create_authenticator = lambda _: None
369
370   if not on_error.report_on_exception_exit(url):
371     print 'Failure to register the handler'
372     return 1
373
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
378
379   if mode == 'crash':
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')
385
386   if mode == 'report':
387     # Generate a manual report without an exception frame. Also set the version
388     # value.
389     setattr(sys.modules['__main__'], '__version__', '123')
390     on_error.report('Oh dang')
391
392   if mode == 'exception':
393     # Report from inside an exception frame.
394     try:
395       raise TypeError('You are not my type')
396     except TypeError:
397       on_error.report('Really')
398
399   if mode == 'exception_no_msg':
400     # Report from inside an exception frame.
401     try:
402       raise TypeError('You are not my type #2')
403     except TypeError:
404       on_error.report(None)
405   return 0
406
407
408 if __name__ == '__main__':
409   # Ignore _DISABLE_ENVVAR if set.
410   os.environ.pop(on_error._DISABLE_ENVVAR, None)
411
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]))
414
415   if '-v' in sys.argv:
416     unittest.TestCase.maxDiff = None
417   logging.basicConfig(
418       level=logging.DEBUG if '-v' in sys.argv else logging.ERROR)
419   unittest.main()