| 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 branding=Chrome 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 --chrome=out/Release |
| 25 | 25 |
| 26 Many of the components in this test could be reused in other end-to-end crash | 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 | 27 testing. Feel free to open them up for reuse, but please CC chrisha@chromium.org |
| 28 on any associated reviews or bugs! | 28 on any associated reviews or bugs! |
| 29 """ | 29 """ |
| 30 | 30 |
| 31 import BaseHTTPServer | |
| 32 import cgi | |
| 33 import logging | 31 import logging |
| 34 import os | 32 import os |
| 35 import optparse | |
| 36 import pywintypes | |
| 37 import re | |
| 38 import shutil | |
| 39 import socket | |
| 40 import subprocess | |
| 41 import sys | 33 import sys |
| 42 import tempfile | 34 |
| 43 import threading | 35 # Bring in the Kasko module. |
| 44 import time | 36 KASKO_DIR = os.path.join(os.path.dirname(__file__), 'py') |
| 45 import uuid | 37 sys.path.append(KASKO_DIR) |
| 46 import win32api | 38 import kasko |
| 47 import win32com.client | |
| 48 import win32con | |
| 49 import win32event | |
| 50 import win32gui | |
| 51 import win32process | |
| 52 | 39 |
| 53 | 40 |
| 54 _DEFAULT_TIMEOUT = 10 # Seconds. | |
| 55 _LOGGER = logging.getLogger(os.path.basename(__file__)) | 41 _LOGGER = logging.getLogger(os.path.basename(__file__)) |
| 56 | 42 |
| 57 | 43 |
| 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(): | 44 def Main(): |
| 506 options = _ParseCommandLine() | 45 options = kasko.config.ParseCommandLine() |
| 507 | 46 |
| 508 # Generate a temporary directory for use in the tests. | 47 # Generate a temporary directory for use in the tests. |
| 509 with _ScopedTempDir() as temp_dir: | 48 with kasko.util.ScopedTempDir() as temp_dir: |
| 510 # Prevent the temporary directory from self cleaning if requested. | 49 # Prevent the temporary directory from self cleaning if requested. |
| 511 if options.keep_temp_dirs: | 50 if options.keep_temp_dirs: |
| 512 temp_dir_path = temp_dir.release() | 51 temp_dir_path = temp_dir.release() |
| 513 else: | 52 else: |
| 514 temp_dir_path = temp_dir.path | 53 temp_dir_path = temp_dir.path |
| 515 | 54 |
| 516 # Use the specified user data directory if requested. | 55 # Use the specified user data directory if requested. |
| 517 if options.user_data_dir: | 56 if options.user_data_dir: |
| 518 user_data_dir = options.user_data_dir | 57 user_data_dir = options.user_data_dir |
| 519 else: | 58 else: |
| 520 user_data_dir = os.path.join(temp_dir_path, 'user-data-dir') | 59 user_data_dir = os.path.join(temp_dir_path, 'user-data-dir') |
| 521 | 60 |
| 522 kasko_dir = os.path.join(temp_dir_path, 'kasko') | 61 kasko_dir = os.path.join(temp_dir_path, 'kasko') |
| 523 os.makedirs(kasko_dir) | 62 os.makedirs(kasko_dir) |
| 524 | 63 |
| 525 # Launch the test server. | 64 # Launch the test server. |
| 526 server = _CrashServer() | 65 server = kasko.crash_server.CrashServer() |
| 527 with _ScopedStartStop(server): | 66 with kasko.util.ScopedStartStop(server): |
| 528 _LOGGER.info('Started server on port %d', server.port) | 67 _LOGGER.info('Started server on port %d', server.port) |
| 529 | 68 |
| 530 # Configure the environment so Chrome can find the test crash server. | 69 # Configure the environment so Chrome can find the test crash server. |
| 531 os.environ['KASKO_CRASH_SERVER_URL'] = ( | 70 os.environ['KASKO_CRASH_SERVER_URL'] = ( |
| 532 'http://127.0.0.1:%d/crash' % server.port) | 71 'http://127.0.0.1:%d/crash' % server.port) |
| 533 | 72 |
| 534 # Launch Chrome and navigate it to the test URL. | 73 # Launch Chrome and navigate it to the test URL. |
| 535 chrome = _ChromeInstance(options.chromedriver, options.chrome, | 74 chrome = kasko.process.ChromeInstance(options.chromedriver, |
| 536 user_data_dir) | 75 options.chrome, user_data_dir) |
| 537 with _ScopedStartStop(chrome): | 76 with kasko.util.ScopedStartStop(chrome): |
| 538 _LOGGER.info('Navigating to Kasko debug URL') | 77 _LOGGER.info('Navigating to Kasko debug URL') |
| 539 chrome.navigate_to('chrome://kasko/send-report') | 78 chrome.navigate_to('chrome://kasko/send-report') |
| 540 | 79 |
| 541 _LOGGER.info('Waiting for Kasko report') | 80 _LOGGER.info('Waiting for Kasko report') |
| 542 if not server.wait_for_report(10): | 81 if not server.wait_for_report(10): |
| 543 raise Exception('No Kasko report received.') | 82 raise Exception('No Kasko report received.') |
| 544 | 83 |
| 545 report = server.crash(0) | 84 report = server.crash(0) |
| 546 for key in sorted(report.keys()): | 85 kasko.report.LogCrashKeys(report) |
| 547 val = report[key][0] | 86 kasko.report.ValidateCrashReport(report, |
| 548 if (len(val) < 64): | 87 {'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 | 88 |
| 561 return 0 | 89 return 0 |
| 562 | 90 |
| 563 | 91 |
| 564 if __name__ == '__main__': | 92 if __name__ == '__main__': |
| 565 sys.exit(Main()) | 93 sys.exit(Main()) |
| OLD | NEW |