Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(909)

Unified Diff: chrome/test/kasko/kasko_integration_test.py

Issue 1582613002: [win] Create a SyzyAsan/Chrome/Kasko/Crashpad integration test. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Slight refactor to use pre-instrumented binaries. Created 4 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/test/kasko/kasko_integration_test.py
diff --git a/chrome/test/kasko/kasko_integration_test.py b/chrome/test/kasko/kasko_integration_test.py
index b23446b7ff3a208a7703a22d795407b9a4592c3e..9e50f589c39d1c014e38d15f8cc3d5fe1734a0e6 100755
--- a/chrome/test/kasko/kasko_integration_test.py
+++ b/chrome/test/kasko/kasko_integration_test.py
@@ -21,492 +21,31 @@ Typical usage (assuming in root 'src' directory):
- build the release Chrome binaries:
ninja -C out\Release chrome.exe
- run the test:
- python chrome/test/kasko/kasko_integration_test.py --chrome-dir=out/Release
+ python chrome/test/kasko/kasko_integration_test.py --chrome=out/Release
Many of the components in this test could be reused in other end-to-end crash
testing. Feel free to open them up for reuse, but please CC chrisha@chromium.org
on any associated reviews or bugs!
"""
-import BaseHTTPServer
-import cgi
import logging
import os
-import optparse
-import pywintypes
-import re
-import shutil
-import socket
-import subprocess
import sys
-import tempfile
-import threading
-import time
-import uuid
-import win32api
-import win32com.client
-import win32con
-import win32event
-import win32gui
-import win32process
+# Bring in the Kasko module.
+KASKO_DIR = os.path.join(os.path.dirname(__file__), 'py')
+sys.path.append(KASKO_DIR)
+import kasko
-_DEFAULT_TIMEOUT = 10 # Seconds.
-_LOGGER = logging.getLogger(os.path.basename(__file__))
-
-
-class _TimeoutException(Exception):
- """Exception used to indicate a timeout has occurred."""
- pass
-
-
-class _StoppableHTTPServer(BaseHTTPServer.HTTPServer):
- """An extension of BaseHTTPServer that uses timeouts and is interruptable."""
-
- def server_bind(self):
- BaseHTTPServer.HTTPServer.server_bind(self)
- self.socket.settimeout(1)
- self.run_ = True
-
- def get_request(self):
- while self.run_:
- try:
- sock, addr = self.socket.accept()
- sock.settimeout(None)
- return (sock, addr)
- except socket.timeout:
- pass
-
- def stop(self):
- self.run_ = False
-
- def serve(self):
- while self.run_:
- self.handle_request()
-
-
-class _CrashServer(object):
- """A simple crash server for testing."""
-
- def __init__(self):
- self.server_ = None
- self.lock_ = threading.Lock()
- self.crashes_ = [] # Under lock_.
-
- def crash(self, index):
- """Accessor for the list of crashes."""
- with self.lock_:
- if index >= len(self.crashes_):
- return None
- return self.crashes_[index]
-
- @property
- def port(self):
- """Returns the port associated with the server."""
- if not self.server_:
- return 0
- return self.server_.server_port
-
- def start(self):
- """Starts the server on another thread. Call from main thread only."""
- page_handler = self.multipart_form_handler()
- self.server_ = _StoppableHTTPServer(('127.0.0.1', 0), page_handler)
- self.thread_ = self.server_thread()
- self.thread_.start()
-
- def stop(self):
- """Stops the running server. Call from main thread only."""
- self.server_.stop()
- self.thread_.join()
- self.server_ = None
- self.thread_ = None
-
- def wait_for_report(self, timeout):
- """Waits until the server has received a crash report.
-
- Returns True if the a report has been received in the given time, or False
- if a timeout occurred. Since Python condition variables have no notion of
- timeout this is, sadly, a busy loop on the calling thread.
- """
- started = time.time()
- elapsed = 0
- while elapsed < timeout:
- with self.lock_:
- if len(self.crashes_):
- return True
- time.sleep(0.1)
- elapsed = time.time() - started
-
- return False
-
-
- def multipart_form_handler(crash_server):
- """Returns a multi-part form handler class for use with a BaseHTTPServer."""
-
- class MultipartFormHandler(BaseHTTPServer.BaseHTTPRequestHandler):
- """A multi-part form handler that processes crash reports.
-
- This class only handles multipart form POST messages, with all other
- requests by default returning a '501 not implemented' error.
- """
-
- def __init__(self, request, client_address, socket_server):
- BaseHTTPServer.BaseHTTPRequestHandler.__init__(
- self, request, client_address, socket_server)
-
- def log_message(self, format, *args):
- _LOGGER.debug(format, *args)
-
- def do_POST(self):
- """Handles POST messages contained multipart form data."""
- content_type, parameters = cgi.parse_header(
- self.headers.getheader('content-type'))
- if content_type != 'multipart/form-data':
- raise Exception('Unsupported Content-Type: ' + content_type)
- post_multipart = cgi.parse_multipart(self.rfile, parameters)
-
- # Save the crash report.
- report = dict(post_multipart.items())
- report_id = str(uuid.uuid4())
- report['report-id'] = [report_id]
- with crash_server.lock_:
- crash_server.crashes_.append(report)
-
- # Send the response.
- self.send_response(200)
- self.send_header("Content-Type", "text/plain")
- self.end_headers()
- self.wfile.write(report_id)
-
- return MultipartFormHandler
-
- def server_thread(crash_server):
- """Returns a thread that hosts the webserver."""
-
- class ServerThread(threading.Thread):
- def run(self):
- crash_server.server_.serve()
-
- return ServerThread()
-
-
-class _ScopedTempDir(object):
- """A class that creates a scoped temporary directory."""
-
- def __init__(self):
- self.path_ = None
-
- def __enter__(self):
- """Creates the temporary directory and initializes |path|."""
- self.path_ = tempfile.mkdtemp(prefix='kasko_integration_')
- return self
-
- def __exit__(self, *args, **kwargs):
- """Destroys the temporary directory."""
- if self.path_ is None:
- return
- shutil.rmtree(self.path_)
-
- @property
- def path(self):
- return self.path_
-
- def release(self):
- path = self.path_
- self.path_ = None
- return path
-
-
-class _ScopedStartStop(object):
- """Utility class for calling 'start' and 'stop' within a scope."""
-
- def __init__(self, service, start=None, stop=None):
- self.service_ = service
-
- if start is None:
- self.start_ = lambda x: x.start()
- else:
- self.start_ = start
-
- if stop is None:
- self.stop_ = lambda x: x.stop()
- else:
- self.stop_ = stop
-
- def __enter__(self):
- self.start_(self.service_)
- return self
-
- def __exit__(self, *args, **kwargs):
- if self.service_:
- self.stop_(self.service_)
-
- @property
- def service(self):
- """Returns the encapsulated service, retaining ownership."""
- return self.service_
- def release(self):
- """Relinquishes ownership of the encapsulated service and returns it."""
- service = self.service_
- self.service_ = None
- return service
-
-
-def _FindChromeProcessId(user_data_dir, timeout=_DEFAULT_TIMEOUT):
- """Finds the process ID of a given Chrome instance."""
- udd = os.path.abspath(user_data_dir)
-
- # Find the message window.
- started = time.time()
- elapsed = 0
- msg_win = None
- while msg_win is None:
- try:
- win = win32gui.FindWindowEx(None, None, 'Chrome_MessageWindow', udd)
- if win != 0:
- msg_win = win
- break
- except pywintypes.error:
- continue
-
- time.sleep(0.1)
- elapsed = time.time() - started
- if elapsed >= timeout:
- raise _TimeoutException()
-
- # Get the process ID associated with the message window.
- tid, pid = win32process.GetWindowThreadProcessId(msg_win)
-
- return pid
-
-
-def _ShutdownProcess(process_id, timeout, force=False):
- """Attempts to nicely close the specified process.
-
- Returns the exit code on success. Raises an error on failure.
- """
-
- # Open the process in question, so we can wait for it to exit.
- permissions = win32con.SYNCHRONIZE | win32con.PROCESS_QUERY_INFORMATION
- process_handle = win32api.OpenProcess(permissions, False, process_id)
-
- # Loop around to periodically retry to close Chrome.
- started = time.time()
- elapsed = 0
- while True:
- _LOGGER.debug('Shutting down process with PID=%d.', process_id)
-
- with open(os.devnull, 'w') as f:
- cmd = ['taskkill.exe', '/PID', str(process_id)]
- if force:
- cmd.append('/F')
- subprocess.call(cmd, shell=True, stdout=f, stderr=f)
-
- # Wait at most 2 seconds after each call to taskkill.
- curr_timeout_ms = int(max(2, timeout - elapsed) * 1000)
-
- _LOGGER.debug('Waiting for process with PID=%d to exit.', process_id)
- result = win32event.WaitForSingleObject(process_handle, curr_timeout_ms)
- # Exit the loop on successful wait.
- if result == win32event.WAIT_OBJECT_0:
- break
-
- elapsed = time.time() - started
- if elapsed > timeout:
- _LOGGER.debug('Timeout waiting for process to exit.')
- raise _TimeoutException()
-
- exit_status = win32process.GetExitCodeProcess(process_handle)
- process_handle.Close()
- _LOGGER.debug('Process exited with status %d.', exit_status)
-
- return exit_status
-
-
-def _WmiTimeToLocalEpoch(wmitime):
- """Converts a WMI time string to a Unix epoch time."""
- # The format of WMI times is: yyyymmddHHMMSS.xxxxxx[+-]UUU, where
- # UUU is the number of minutes between local time and UTC.
- m = re.match('^(?P<year>\d{4})(?P<month>\d{2})(?P<day>\d{2})'
- '(?P<hour>\d{2})(?P<minutes>\d{2})(?P<seconds>\d{2}\.\d+)'
- '(?P<offset>[+-]\d{3})$', wmitime)
- if not m:
- raise Exception('Invalid WMI time string.')
-
- # This parses the time as a local time.
- t = time.mktime(time.strptime(wmitime[0:14], '%Y%m%d%H%M%S'))
-
- # Add the fractional part of the seconds that wasn't parsed by strptime.
- s = float(m.group('seconds'))
- t += s - int(s)
-
- return t
-
-
-def _GetProcessCreationDate(pid):
- """Returns the process creation date as local unix epoch time."""
- wmi = win32com.client.GetObject('winmgmts:')
- procs = wmi.ExecQuery(
- 'select CreationDate from Win32_Process where ProcessId = %s' % pid)
- for proc in procs:
- return _WmiTimeToLocalEpoch(proc.Properties_('CreationDate').Value)
- raise Exception('Unable to find process with PID %d.' % pid)
-
-
-def _ShutdownChildren(parent_pid, child_exe, started_after, started_before,
- timeout=_DEFAULT_TIMEOUT, force=False):
- """Shuts down any lingering child processes of a given parent.
-
- This is an inherently racy thing to do as process IDs are aggressively reused
- on Windows. Filtering by a valid known |started_after| and |started_before|
- timestamp, as well as by the executable of the child process resolves this
- issue. Ugh.
- """
- started = time.time()
- wmi = win32com.client.GetObject('winmgmts:')
- _LOGGER.debug('Shutting down lingering children processes.')
- for proc in wmi.InstancesOf('Win32_Process'):
- if proc.Properties_('ParentProcessId').Value != parent_pid:
- continue
- if proc.Properties_('ExecutablePath').Value != child_exe:
- continue
- t = _WmiTimeToLocalEpoch(proc.Properties_('CreationDate').Value)
- if t <= started_after or t >= started_before:
- continue
- pid = proc.Properties_('ProcessId').Value
- remaining = max(0, started + timeout - time.time())
- _ShutdownProcess(pid, remaining, force=force)
-
-
-class _ChromeInstance(object):
- """A class encapsulating a running instance of Chrome for testing."""
-
- def __init__(self, chromedriver, chrome, user_data_dir):
- self.chromedriver_ = os.path.abspath(chromedriver)
- self.chrome_ = os.path.abspath(chrome)
- self.user_data_dir_ = user_data_dir
-
- def start(self, timeout=_DEFAULT_TIMEOUT):
- capabilities = {
- 'chromeOptions': {
- 'args': [
- # This allows automated navigation to chrome:// URLs.
- '--enable-gpu-benchmarking',
- '--user-data-dir=%s' % self.user_data_dir_,
- ],
- 'binary': self.chrome_,
- }
- }
-
- # Use a _ScopedStartStop helper so the service and driver clean themselves
- # up in case of any exceptions.
- _LOGGER.info('Starting chromedriver')
- with _ScopedStartStop(service.Service(self.chromedriver_)) as \
- scoped_service:
- _LOGGER.info('Starting chrome')
- with _ScopedStartStop(webdriver.Remote(scoped_service.service.service_url,
- capabilities),
- start=lambda x: None, stop=lambda x: x.quit()) as \
- scoped_driver:
- self.pid_ = _FindChromeProcessId(self.user_data_dir_, timeout)
- self.started_at_ = _GetProcessCreationDate(self.pid_)
- _LOGGER.debug('Chrome launched.')
- self.driver_ = scoped_driver.release()
- self.service_ = scoped_service.release()
-
-
- def stop(self, timeout=_DEFAULT_TIMEOUT):
- started = time.time()
- self.driver_.quit()
- self.stopped_at_ = time.time()
- self.service_.stop()
- self.driver_ = None
- self.service = None
-
- # Ensure that any lingering children processes are torn down as well. This
- # is generally racy on Windows, but is gated based on parent process ID,
- # child executable, and start time of the child process. These criteria
- # ensure we don't go indiscriminately killing processes.
- remaining = max(0, started + timeout - time.time())
- _ShutdownChildren(self.pid_, self.chrome_, self.started_at_,
- self.stopped_at_, remaining, force=True)
-
- def navigate_to(self, url):
- """Navigates the running Chrome instance to the provided URL."""
- self.driver_.get(url)
-
-
-def _ParseCommandLine():
- """Parses the command-line and returns an options structure."""
- self_dir = os.path.dirname(__file__)
- src_dir = os.path.abspath(os.path.join(self_dir, '..', '..', '..'))
-
- option_parser = optparse.OptionParser()
- option_parser.add_option('--chrome', dest='chrome', type='string',
- default=os.path.join(src_dir, 'out', 'Release', 'chrome.exe'),
- help='Path to chrome.exe. Defaults to $SRC/out/Release/chrome.exe')
- option_parser.add_option('--chromedriver', dest='chromedriver',
- type='string', help='Path to the chromedriver.exe. By default will look '
- 'alongside chrome.exe.')
- option_parser.add_option('--keep-temp-dirs', action='store_true',
- default=False, help='Prevents temporary directories from being deleted.')
- option_parser.add_option('--quiet', dest='log_level', action='store_const',
- default=logging.INFO, const=logging.ERROR,
- help='Disables all output except for errors.')
- option_parser.add_option('--user-data-dir', dest='user_data_dir',
- type='string', help='User data directory to use. Defaults to using a '
- 'temporary one.')
- option_parser.add_option('--verbose', dest='log_level', action='store_const',
- default=logging.INFO, const=logging.DEBUG,
- help='Enables verbose logging.')
- option_parser.add_option('--webdriver', type='string',
- default=os.path.join(src_dir, 'third_party', 'webdriver', 'pylib'),
- help='Specifies the directory where the python installation of webdriver '
- '(selenium) can be found. Specify an empty string to use the system '
- 'installation. Defaults to $SRC/third_party/webdriver/pylib')
- options, args = option_parser.parse_args()
- if args:
- option_parser.error('Unexpected arguments: %s' % args)
-
- # Validate chrome.exe exists.
- if not os.path.isfile(options.chrome):
- option_parser.error('chrome.exe not found')
-
- # Use default chromedriver.exe if necessary, and validate it exists.
- if not options.chromedriver:
- options.chromedriver = os.path.join(os.path.dirname(options.chrome),
- 'chromedriver.exe')
- if not os.path.isfile(options.chromedriver):
- option_parser.error('chromedriver.exe not found')
-
- # If specified, ensure the webdriver parameters is a directory.
- if options.webdriver and not os.path.isdir(options.webdriver):
- option_parser.error('Invalid webdriver directory.')
-
- # Configure logging.
- logging.basicConfig(level=options.log_level)
-
- _LOGGER.debug('Using chrome path: %s', options.chrome)
- _LOGGER.debug('Using chromedriver path: %s', options.chromedriver)
- _LOGGER.debug('Using webdriver path: %s', options.webdriver)
-
- # Import webdriver and selenium.
- global webdriver
- global service
- if options.webdriver:
- sys.path.append(options.webdriver)
- from selenium import webdriver
- import selenium.webdriver.chrome.service as service
-
- return options
+_LOGGER = logging.getLogger(os.path.basename(__file__))
def Main():
- options = _ParseCommandLine()
+ options = kasko.config.ParseCommandLine()
# Generate a temporary directory for use in the tests.
- with _ScopedTempDir() as temp_dir:
+ with kasko.util.ScopedTempDir() as temp_dir:
# Prevent the temporary directory from self cleaning if requested.
if options.keep_temp_dirs:
temp_dir_path = temp_dir.release()
@@ -523,8 +62,8 @@ def Main():
os.makedirs(kasko_dir)
# Launch the test server.
- server = _CrashServer()
- with _ScopedStartStop(server):
+ server = kasko.crash_server.CrashServer()
+ with kasko.util.ScopedStartStop(server):
_LOGGER.info('Started server on port %d', server.port)
# Configure the environment so Chrome can find the test crash server.
@@ -532,9 +71,9 @@ def Main():
'http://127.0.0.1:%d/crash' % server.port)
# Launch Chrome and navigate it to the test URL.
- chrome = _ChromeInstance(options.chromedriver, options.chrome,
- user_data_dir)
- with _ScopedStartStop(chrome):
+ chrome = kasko.process.ChromeInstance(options.chromedriver,
+ options.chrome, user_data_dir)
+ with kasko.util.ScopedStartStop(chrome):
_LOGGER.info('Navigating to Kasko debug URL')
chrome.navigate_to('chrome://kasko/send-report')
@@ -543,20 +82,9 @@ def Main():
raise Exception('No Kasko report received.')
report = server.crash(0)
- for key in sorted(report.keys()):
- val = report[key][0]
- if (len(val) < 64):
- _LOGGER.debug('Got crashkey "%s": "%s"', key, val)
- else:
- _LOGGER.debug('Got crashkey "%s": ...%d bytes...', key, len(val))
-
- expected_keys = {
- 'kasko-set-crash-key-value-impl': 'SetCrashKeyValueImpl',
- 'guid': 'GetCrashKeysForKasko'}
- for expected_key, error in expected_keys.iteritems():
- if expected_key not in report:
- _LOGGER.error('Missing expected "%s" crash key.', expected_key)
- raise Exception('"%s" integration appears broken.' % error)
+ kasko.report.LogCrashKeys(report)
+ kasko.report.ValidateCrashReport(report,
+ {'kasko-set-crash-key-value-impl': 'SetCrashKeyValueImpl'})
return 0
« no previous file with comments | « no previous file | chrome/test/kasko/py/kasko/__init__.py » ('j') | chrome/test/kasko/syzyasan_integration_test.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698