OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright 2015 The Chromium Authors. All rights reserved. | 2 # Copyright 2015 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """A Windows-only end-to-end integration test for Kasko, Chrome and Crashpad. | 6 """A Windows-only end-to-end integration test for Kasko, Chrome and Crashpad. |
7 | 7 |
8 This test ensures that the interface between Kasko and Chrome and Crashpad works | 8 This test ensures that the interface between Kasko and Chrome and Crashpad works |
9 as expected. The test causes Kasko to set certain crash keys and invoke a crash | 9 as expected. The test causes Kasko to set certain crash keys and invoke a crash |
10 report, which is in turn delivered to a locally hosted test crash server. If the | 10 report, which is in turn delivered to a locally hosted test crash server. If the |
11 crash report is received intact with the expected crash keys then all is well. | 11 crash report is received intact with the expected crash keys then all is well. |
12 | 12 |
13 Note that this test only works against non-component Release and Official builds | 13 Note that this test only works against non-component Release and Official builds |
14 of Chrome with Chrome branding, and attempting to use it with anything else will | 14 of Chrome with Chrome branding, and attempting to use it with anything else will |
15 most likely lead to constant failures. | 15 most likely lead to constant failures. |
16 | 16 |
17 Typical usage (assuming in root 'src' directory): | 17 Typical usage (assuming in root 'src' directory): |
18 | 18 |
19 - generate project files with the following GYP variables: | 19 - generate project files with the following GYP variables: |
20 branding=Chrome syzyasan=1 win_z7=0 chromium_win_pch=0 | 20 syzyasan=1 win_z7=0 chromium_win_pch=0 |
21 - build the release Chrome binaries: | 21 - build the release Chrome binaries: |
22 ninja -C out\Release chrome.exe | 22 ninja -C out\Release chrome.exe |
23 - run the test: | 23 - run the test: |
24 python chrome/test/kasko/kasko_integration_test.py --chrome-dir=out/Release | 24 python chrome/test/kasko/kasko_integration_test.py |
25 | |
26 Many of the components in this test could be reused in other end-to-end crash | |
27 testing. Feel free to open them up for reuse, but please CC chrisha@chromium.org | |
28 on any associated reviews or bugs! | |
29 """ | 25 """ |
30 | 26 |
31 import BaseHTTPServer | |
32 import cgi | |
33 import logging | 27 import logging |
34 import os | 28 import os |
35 import optparse | |
36 import pywintypes | |
37 import re | |
38 import shutil | |
39 import socket | |
40 import subprocess | |
41 import sys | 29 import sys |
42 import tempfile | 30 |
43 import threading | 31 # Bring in the Kasko module. |
44 import time | 32 KASKO_DIR = os.path.join(os.path.dirname(__file__), 'py') |
45 import uuid | 33 sys.path.append(KASKO_DIR) |
46 import win32api | 34 import kasko |
47 import win32com.client | |
48 import win32con | |
49 import win32event | |
50 import win32gui | |
51 import win32process | |
52 | 35 |
53 | 36 |
54 _DEFAULT_TIMEOUT = 10 # Seconds. | |
55 _LOGGER = logging.getLogger(os.path.basename(__file__)) | 37 _LOGGER = logging.getLogger(os.path.basename(__file__)) |
56 | 38 |
57 | 39 |
58 class _TimeoutException(Exception): | |
59 """Exception used to indicate a timeout has occurred.""" | |
60 pass | |
61 | |
62 | |
63 class _StoppableHTTPServer(BaseHTTPServer.HTTPServer): | |
64 """An extension of BaseHTTPServer that uses timeouts and is interruptable.""" | |
65 | |
66 def server_bind(self): | |
67 BaseHTTPServer.HTTPServer.server_bind(self) | |
68 self.socket.settimeout(1) | |
69 self.run_ = True | |
70 | |
71 def get_request(self): | |
72 while self.run_: | |
73 try: | |
74 sock, addr = self.socket.accept() | |
75 sock.settimeout(None) | |
76 return (sock, addr) | |
77 except socket.timeout: | |
78 pass | |
79 | |
80 def stop(self): | |
81 self.run_ = False | |
82 | |
83 def serve(self): | |
84 while self.run_: | |
85 self.handle_request() | |
86 | |
87 | |
88 class _CrashServer(object): | |
89 """A simple crash server for testing.""" | |
90 | |
91 def __init__(self): | |
92 self.server_ = None | |
93 self.lock_ = threading.Lock() | |
94 self.crashes_ = [] # Under lock_. | |
95 | |
96 def crash(self, index): | |
97 """Accessor for the list of crashes.""" | |
98 with self.lock_: | |
99 if index >= len(self.crashes_): | |
100 return None | |
101 return self.crashes_[index] | |
102 | |
103 @property | |
104 def port(self): | |
105 """Returns the port associated with the server.""" | |
106 if not self.server_: | |
107 return 0 | |
108 return self.server_.server_port | |
109 | |
110 def start(self): | |
111 """Starts the server on another thread. Call from main thread only.""" | |
112 page_handler = self.multipart_form_handler() | |
113 self.server_ = _StoppableHTTPServer(('127.0.0.1', 0), page_handler) | |
114 self.thread_ = self.server_thread() | |
115 self.thread_.start() | |
116 | |
117 def stop(self): | |
118 """Stops the running server. Call from main thread only.""" | |
119 self.server_.stop() | |
120 self.thread_.join() | |
121 self.server_ = None | |
122 self.thread_ = None | |
123 | |
124 def wait_for_report(self, timeout): | |
125 """Waits until the server has received a crash report. | |
126 | |
127 Returns True if the a report has been received in the given time, or False | |
128 if a timeout occurred. Since Python condition variables have no notion of | |
129 timeout this is, sadly, a busy loop on the calling thread. | |
130 """ | |
131 started = time.time() | |
132 elapsed = 0 | |
133 while elapsed < timeout: | |
134 with self.lock_: | |
135 if len(self.crashes_): | |
136 return True | |
137 time.sleep(0.1) | |
138 elapsed = time.time() - started | |
139 | |
140 return False | |
141 | |
142 | |
143 def multipart_form_handler(crash_server): | |
144 """Returns a multi-part form handler class for use with a BaseHTTPServer.""" | |
145 | |
146 class MultipartFormHandler(BaseHTTPServer.BaseHTTPRequestHandler): | |
147 """A multi-part form handler that processes crash reports. | |
148 | |
149 This class only handles multipart form POST messages, with all other | |
150 requests by default returning a '501 not implemented' error. | |
151 """ | |
152 | |
153 def __init__(self, request, client_address, socket_server): | |
154 BaseHTTPServer.BaseHTTPRequestHandler.__init__( | |
155 self, request, client_address, socket_server) | |
156 | |
157 def log_message(self, format, *args): | |
158 _LOGGER.debug(format, *args) | |
159 | |
160 def do_POST(self): | |
161 """Handles POST messages contained multipart form data.""" | |
162 content_type, parameters = cgi.parse_header( | |
163 self.headers.getheader('content-type')) | |
164 if content_type != 'multipart/form-data': | |
165 raise Exception('Unsupported Content-Type: ' + content_type) | |
166 post_multipart = cgi.parse_multipart(self.rfile, parameters) | |
167 | |
168 # Save the crash report. | |
169 report = dict(post_multipart.items()) | |
170 report_id = str(uuid.uuid4()) | |
171 report['report-id'] = [report_id] | |
172 with crash_server.lock_: | |
173 crash_server.crashes_.append(report) | |
174 | |
175 # Send the response. | |
176 self.send_response(200) | |
177 self.send_header("Content-Type", "text/plain") | |
178 self.end_headers() | |
179 self.wfile.write(report_id) | |
180 | |
181 return MultipartFormHandler | |
182 | |
183 def server_thread(crash_server): | |
184 """Returns a thread that hosts the webserver.""" | |
185 | |
186 class ServerThread(threading.Thread): | |
187 def run(self): | |
188 crash_server.server_.serve() | |
189 | |
190 return ServerThread() | |
191 | |
192 | |
193 class _ScopedTempDir(object): | |
194 """A class that creates a scoped temporary directory.""" | |
195 | |
196 def __init__(self): | |
197 self.path_ = None | |
198 | |
199 def __enter__(self): | |
200 """Creates the temporary directory and initializes |path|.""" | |
201 self.path_ = tempfile.mkdtemp(prefix='kasko_integration_') | |
202 return self | |
203 | |
204 def __exit__(self, *args, **kwargs): | |
205 """Destroys the temporary directory.""" | |
206 if self.path_ is None: | |
207 return | |
208 shutil.rmtree(self.path_) | |
209 | |
210 @property | |
211 def path(self): | |
212 return self.path_ | |
213 | |
214 def release(self): | |
215 path = self.path_ | |
216 self.path_ = None | |
217 return path | |
218 | |
219 | |
220 class _ScopedStartStop(object): | |
221 """Utility class for calling 'start' and 'stop' within a scope.""" | |
222 | |
223 def __init__(self, service, start=None, stop=None): | |
224 self.service_ = service | |
225 | |
226 if start is None: | |
227 self.start_ = lambda x: x.start() | |
228 else: | |
229 self.start_ = start | |
230 | |
231 if stop is None: | |
232 self.stop_ = lambda x: x.stop() | |
233 else: | |
234 self.stop_ = stop | |
235 | |
236 def __enter__(self): | |
237 self.start_(self.service_) | |
238 return self | |
239 | |
240 def __exit__(self, *args, **kwargs): | |
241 if self.service_: | |
242 self.stop_(self.service_) | |
243 | |
244 @property | |
245 def service(self): | |
246 """Returns the encapsulated service, retaining ownership.""" | |
247 return self.service_ | |
248 | |
249 def release(self): | |
250 """Relinquishes ownership of the encapsulated service and returns it.""" | |
251 service = self.service_ | |
252 self.service_ = None | |
253 return service | |
254 | |
255 | |
256 def _FindChromeProcessId(user_data_dir, timeout=_DEFAULT_TIMEOUT): | |
257 """Finds the process ID of a given Chrome instance.""" | |
258 udd = os.path.abspath(user_data_dir) | |
259 | |
260 # Find the message window. | |
261 started = time.time() | |
262 elapsed = 0 | |
263 msg_win = None | |
264 while msg_win is None: | |
265 try: | |
266 win = win32gui.FindWindowEx(None, None, 'Chrome_MessageWindow', udd) | |
267 if win != 0: | |
268 msg_win = win | |
269 break | |
270 except pywintypes.error: | |
271 continue | |
272 | |
273 time.sleep(0.1) | |
274 elapsed = time.time() - started | |
275 if elapsed >= timeout: | |
276 raise _TimeoutException() | |
277 | |
278 # Get the process ID associated with the message window. | |
279 tid, pid = win32process.GetWindowThreadProcessId(msg_win) | |
280 | |
281 return pid | |
282 | |
283 | |
284 def _ShutdownProcess(process_id, timeout, force=False): | |
285 """Attempts to nicely close the specified process. | |
286 | |
287 Returns the exit code on success. Raises an error on failure. | |
288 """ | |
289 | |
290 # Open the process in question, so we can wait for it to exit. | |
291 permissions = win32con.SYNCHRONIZE | win32con.PROCESS_QUERY_INFORMATION | |
292 process_handle = win32api.OpenProcess(permissions, False, process_id) | |
293 | |
294 # Loop around to periodically retry to close Chrome. | |
295 started = time.time() | |
296 elapsed = 0 | |
297 while True: | |
298 _LOGGER.debug('Shutting down process with PID=%d.', process_id) | |
299 | |
300 with open(os.devnull, 'w') as f: | |
301 cmd = ['taskkill.exe', '/PID', str(process_id)] | |
302 if force: | |
303 cmd.append('/F') | |
304 subprocess.call(cmd, shell=True, stdout=f, stderr=f) | |
305 | |
306 # Wait at most 2 seconds after each call to taskkill. | |
307 curr_timeout_ms = int(max(2, timeout - elapsed) * 1000) | |
308 | |
309 _LOGGER.debug('Waiting for process with PID=%d to exit.', process_id) | |
310 result = win32event.WaitForSingleObject(process_handle, curr_timeout_ms) | |
311 # Exit the loop on successful wait. | |
312 if result == win32event.WAIT_OBJECT_0: | |
313 break | |
314 | |
315 elapsed = time.time() - started | |
316 if elapsed > timeout: | |
317 _LOGGER.debug('Timeout waiting for process to exit.') | |
318 raise _TimeoutException() | |
319 | |
320 exit_status = win32process.GetExitCodeProcess(process_handle) | |
321 process_handle.Close() | |
322 _LOGGER.debug('Process exited with status %d.', exit_status) | |
323 | |
324 return exit_status | |
325 | |
326 | |
327 def _WmiTimeToLocalEpoch(wmitime): | |
328 """Converts a WMI time string to a Unix epoch time.""" | |
329 # The format of WMI times is: yyyymmddHHMMSS.xxxxxx[+-]UUU, where | |
330 # UUU is the number of minutes between local time and UTC. | |
331 m = re.match('^(?P<year>\d{4})(?P<month>\d{2})(?P<day>\d{2})' | |
332 '(?P<hour>\d{2})(?P<minutes>\d{2})(?P<seconds>\d{2}\.\d+)' | |
333 '(?P<offset>[+-]\d{3})$', wmitime) | |
334 if not m: | |
335 raise Exception('Invalid WMI time string.') | |
336 | |
337 # This parses the time as a local time. | |
338 t = time.mktime(time.strptime(wmitime[0:14], '%Y%m%d%H%M%S')) | |
339 | |
340 # Add the fractional part of the seconds that wasn't parsed by strptime. | |
341 s = float(m.group('seconds')) | |
342 t += s - int(s) | |
343 | |
344 return t | |
345 | |
346 | |
347 def _GetProcessCreationDate(pid): | |
348 """Returns the process creation date as local unix epoch time.""" | |
349 wmi = win32com.client.GetObject('winmgmts:') | |
350 procs = wmi.ExecQuery( | |
351 'select CreationDate from Win32_Process where ProcessId = %s' % pid) | |
352 for proc in procs: | |
353 return _WmiTimeToLocalEpoch(proc.Properties_('CreationDate').Value) | |
354 raise Exception('Unable to find process with PID %d.' % pid) | |
355 | |
356 | |
357 def _ShutdownChildren(parent_pid, child_exe, started_after, started_before, | |
358 timeout=_DEFAULT_TIMEOUT, force=False): | |
359 """Shuts down any lingering child processes of a given parent. | |
360 | |
361 This is an inherently racy thing to do as process IDs are aggressively reused | |
362 on Windows. Filtering by a valid known |started_after| and |started_before| | |
363 timestamp, as well as by the executable of the child process resolves this | |
364 issue. Ugh. | |
365 """ | |
366 started = time.time() | |
367 wmi = win32com.client.GetObject('winmgmts:') | |
368 _LOGGER.debug('Shutting down lingering children processes.') | |
369 for proc in wmi.InstancesOf('Win32_Process'): | |
370 if proc.Properties_('ParentProcessId').Value != parent_pid: | |
371 continue | |
372 if proc.Properties_('ExecutablePath').Value != child_exe: | |
373 continue | |
374 t = _WmiTimeToLocalEpoch(proc.Properties_('CreationDate').Value) | |
375 if t <= started_after or t >= started_before: | |
376 continue | |
377 pid = proc.Properties_('ProcessId').Value | |
378 remaining = max(0, started + timeout - time.time()) | |
379 _ShutdownProcess(pid, remaining, force=force) | |
380 | |
381 | |
382 class _ChromeInstance(object): | |
383 """A class encapsulating a running instance of Chrome for testing.""" | |
384 | |
385 def __init__(self, chromedriver, chrome, user_data_dir): | |
386 self.chromedriver_ = os.path.abspath(chromedriver) | |
387 self.chrome_ = os.path.abspath(chrome) | |
388 self.user_data_dir_ = user_data_dir | |
389 | |
390 def start(self, timeout=_DEFAULT_TIMEOUT): | |
391 capabilities = { | |
392 'chromeOptions': { | |
393 'args': [ | |
394 # This allows automated navigation to chrome:// URLs. | |
395 '--enable-gpu-benchmarking', | |
396 '--user-data-dir=%s' % self.user_data_dir_, | |
397 ], | |
398 'binary': self.chrome_, | |
399 } | |
400 } | |
401 | |
402 # Use a _ScopedStartStop helper so the service and driver clean themselves | |
403 # up in case of any exceptions. | |
404 _LOGGER.info('Starting chromedriver') | |
405 with _ScopedStartStop(service.Service(self.chromedriver_)) as \ | |
406 scoped_service: | |
407 _LOGGER.info('Starting chrome') | |
408 with _ScopedStartStop(webdriver.Remote(scoped_service.service.service_url, | |
409 capabilities), | |
410 start=lambda x: None, stop=lambda x: x.quit()) as \ | |
411 scoped_driver: | |
412 self.pid_ = _FindChromeProcessId(self.user_data_dir_, timeout) | |
413 self.started_at_ = _GetProcessCreationDate(self.pid_) | |
414 _LOGGER.debug('Chrome launched.') | |
415 self.driver_ = scoped_driver.release() | |
416 self.service_ = scoped_service.release() | |
417 | |
418 | |
419 def stop(self, timeout=_DEFAULT_TIMEOUT): | |
420 started = time.time() | |
421 self.driver_.quit() | |
422 self.stopped_at_ = time.time() | |
423 self.service_.stop() | |
424 self.driver_ = None | |
425 self.service = None | |
426 | |
427 # Ensure that any lingering children processes are torn down as well. This | |
428 # is generally racy on Windows, but is gated based on parent process ID, | |
429 # child executable, and start time of the child process. These criteria | |
430 # ensure we don't go indiscriminately killing processes. | |
431 remaining = max(0, started + timeout - time.time()) | |
432 _ShutdownChildren(self.pid_, self.chrome_, self.started_at_, | |
433 self.stopped_at_, remaining, force=True) | |
434 | |
435 def navigate_to(self, url): | |
436 """Navigates the running Chrome instance to the provided URL.""" | |
437 self.driver_.get(url) | |
438 | |
439 | |
440 def _ParseCommandLine(): | |
441 """Parses the command-line and returns an options structure.""" | |
442 self_dir = os.path.dirname(__file__) | |
443 src_dir = os.path.abspath(os.path.join(self_dir, '..', '..', '..')) | |
444 | |
445 option_parser = optparse.OptionParser() | |
446 option_parser.add_option('--chrome', dest='chrome', type='string', | |
447 default=os.path.join(src_dir, 'out', 'Release', 'chrome.exe'), | |
448 help='Path to chrome.exe. Defaults to $SRC/out/Release/chrome.exe') | |
449 option_parser.add_option('--chromedriver', dest='chromedriver', | |
450 type='string', help='Path to the chromedriver.exe. By default will look ' | |
451 'alongside chrome.exe.') | |
452 option_parser.add_option('--keep-temp-dirs', action='store_true', | |
453 default=False, help='Prevents temporary directories from being deleted.') | |
454 option_parser.add_option('--quiet', dest='log_level', action='store_const', | |
455 default=logging.INFO, const=logging.ERROR, | |
456 help='Disables all output except for errors.') | |
457 option_parser.add_option('--user-data-dir', dest='user_data_dir', | |
458 type='string', help='User data directory to use. Defaults to using a ' | |
459 'temporary one.') | |
460 option_parser.add_option('--verbose', dest='log_level', action='store_const', | |
461 default=logging.INFO, const=logging.DEBUG, | |
462 help='Enables verbose logging.') | |
463 option_parser.add_option('--webdriver', type='string', | |
464 default=os.path.join(src_dir, 'third_party', 'webdriver', 'pylib'), | |
465 help='Specifies the directory where the python installation of webdriver ' | |
466 '(selenium) can be found. Specify an empty string to use the system ' | |
467 'installation. Defaults to $SRC/third_party/webdriver/pylib') | |
468 options, args = option_parser.parse_args() | |
469 if args: | |
470 option_parser.error('Unexpected arguments: %s' % args) | |
471 | |
472 # Validate chrome.exe exists. | |
473 if not os.path.isfile(options.chrome): | |
474 option_parser.error('chrome.exe not found') | |
475 | |
476 # Use default chromedriver.exe if necessary, and validate it exists. | |
477 if not options.chromedriver: | |
478 options.chromedriver = os.path.join(os.path.dirname(options.chrome), | |
479 'chromedriver.exe') | |
480 if not os.path.isfile(options.chromedriver): | |
481 option_parser.error('chromedriver.exe not found') | |
482 | |
483 # If specified, ensure the webdriver parameters is a directory. | |
484 if options.webdriver and not os.path.isdir(options.webdriver): | |
485 option_parser.error('Invalid webdriver directory.') | |
486 | |
487 # Configure logging. | |
488 logging.basicConfig(level=options.log_level) | |
489 | |
490 _LOGGER.debug('Using chrome path: %s', options.chrome) | |
491 _LOGGER.debug('Using chromedriver path: %s', options.chromedriver) | |
492 _LOGGER.debug('Using webdriver path: %s', options.webdriver) | |
493 | |
494 # Import webdriver and selenium. | |
495 global webdriver | |
496 global service | |
497 if options.webdriver: | |
498 sys.path.append(options.webdriver) | |
499 from selenium import webdriver | |
500 import selenium.webdriver.chrome.service as service | |
501 | |
502 return options | |
503 | |
504 | |
505 def Main(): | 40 def Main(): |
506 options = _ParseCommandLine() | 41 options = kasko.config.ParseCommandLine() |
507 | 42 |
508 # Generate a temporary directory for use in the tests. | 43 # Generate a temporary directory for use in the tests. |
509 with _ScopedTempDir() as temp_dir: | 44 with kasko.util.ScopedTempDir() as temp_dir: |
510 # Prevent the temporary directory from self cleaning if requested. | 45 # Prevent the temporary directory from self cleaning if requested. |
511 if options.keep_temp_dirs: | 46 if options.keep_temp_dirs: |
512 temp_dir_path = temp_dir.release() | 47 temp_dir_path = temp_dir.release() |
513 else: | 48 else: |
514 temp_dir_path = temp_dir.path | 49 temp_dir_path = temp_dir.path |
515 | 50 |
516 # Use the specified user data directory if requested. | 51 # Use the specified user data directory if requested. |
517 if options.user_data_dir: | 52 if options.user_data_dir: |
518 user_data_dir = options.user_data_dir | 53 user_data_dir = options.user_data_dir |
519 else: | 54 else: |
520 user_data_dir = os.path.join(temp_dir_path, 'user-data-dir') | 55 user_data_dir = os.path.join(temp_dir_path, 'user-data-dir') |
521 | 56 |
522 kasko_dir = os.path.join(temp_dir_path, 'kasko') | 57 kasko_dir = os.path.join(temp_dir_path, 'kasko') |
523 os.makedirs(kasko_dir) | 58 os.makedirs(kasko_dir) |
524 | 59 |
525 # Launch the test server. | 60 # Launch the test server. |
526 server = _CrashServer() | 61 server = kasko.crash_server.CrashServer() |
527 with _ScopedStartStop(server): | 62 with kasko.util.ScopedStartStop(server): |
528 _LOGGER.info('Started server on port %d', server.port) | 63 _LOGGER.info('Started server on port %d', server.port) |
529 | 64 |
530 # Configure the environment so Chrome can find the test crash server. | 65 # Configure the environment so Chrome can find the test crash server. |
531 os.environ['KASKO_CRASH_SERVER_URL'] = ( | 66 os.environ['KASKO_CRASH_SERVER_URL'] = ( |
532 'http://127.0.0.1:%d/crash' % server.port) | 67 'http://127.0.0.1:%d/crash' % server.port) |
533 | 68 |
534 # Launch Chrome and navigate it to the test URL. | 69 # Launch Chrome and navigate it to the test URL. |
535 chrome = _ChromeInstance(options.chromedriver, options.chrome, | 70 chrome = kasko.process.ChromeInstance(options.chromedriver, |
536 user_data_dir) | 71 options.chrome, user_data_dir) |
537 with _ScopedStartStop(chrome): | 72 with kasko.util.ScopedStartStop(chrome): |
538 _LOGGER.info('Navigating to Kasko debug URL') | 73 _LOGGER.info('Navigating to Kasko debug URL') |
539 chrome.navigate_to('chrome://kasko/send-report') | 74 chrome.navigate_to('chrome://kasko/send-report') |
540 | 75 |
541 _LOGGER.info('Waiting for Kasko report') | 76 _LOGGER.info('Waiting for Kasko report') |
542 if not server.wait_for_report(10): | 77 if not server.wait_for_report(10): |
543 raise Exception('No Kasko report received.') | 78 raise Exception('No Kasko report received.') |
544 | 79 |
545 report = server.crash(0) | 80 report = server.crash(0) |
546 for key in sorted(report.keys()): | 81 kasko.report.LogCrashKeys(report) |
547 val = report[key][0] | 82 kasko.report.ValidateCrashReport(report, |
548 if (len(val) < 64): | 83 {'kasko-set-crash-key-value-impl': 'SetCrashKeyValueImpl'}) |
549 _LOGGER.debug('Got crashkey "%s": "%s"', key, val) | |
550 else: | |
551 _LOGGER.debug('Got crashkey "%s": ...%d bytes...', key, len(val)) | |
552 | |
553 expected_keys = { | |
554 'kasko-set-crash-key-value-impl': 'SetCrashKeyValueImpl', | |
555 'guid': 'GetCrashKeysForKasko'} | |
556 for expected_key, error in expected_keys.iteritems(): | |
557 if expected_key not in report: | |
558 _LOGGER.error('Missing expected "%s" crash key.', expected_key) | |
559 raise Exception('"%s" integration appears broken.' % error) | |
560 | 84 |
561 return 0 | 85 return 0 |
562 | 86 |
563 | 87 |
564 if __name__ == '__main__': | 88 if __name__ == '__main__': |
565 sys.exit(Main()) | 89 sys.exit(Main()) |
OLD | NEW |