Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. | 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 import argparse | 5 import argparse |
| 6 import json | 6 import json |
| 7 import os | 7 import os |
| 8 import re | |
| 8 import shlex | 9 import shlex |
| 9 import sys | 10 import sys |
| 10 import time | 11 import time |
| 11 import traceback | 12 import traceback |
| 12 | 13 |
| 13 sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, | 14 sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, |
| 14 os.pardir, 'third_party', 'webdriver', 'pylib')) | 15 os.pardir, 'third_party', 'webdriver', 'pylib')) |
| 15 from selenium import webdriver | 16 from selenium import webdriver |
| 16 from selenium.webdriver.chrome.options import Options | 17 from selenium.webdriver.chrome.options import Options |
| 17 | 18 |
| 18 # TODO(robertogden) add logging | 19 # TODO(robertogden): Add logging. |
| 19 | 20 |
| 20 | 21 |
| 21 def ParseFlags(): | 22 def ParseFlags(): |
| 22 """ | 23 """ |
| 23 Parses the given command line arguments and returns an object with the flags | 24 Parses the given command line arguments. |
|
sclittle
2016/12/06 18:11:30
tiny, tiny nit: Thanks for changing the style here
Robert Ogden
2016/12/06 18:43:00
Done.
| |
| 24 as properties. | 25 |
| 26 Returns: | |
| 27 A new Namespace object with class properties for each argument added below. | |
| 28 See pydoc for argparse. | |
| 25 """ | 29 """ |
| 26 parser = argparse.ArgumentParser() | 30 parser = argparse.ArgumentParser() |
| 27 parser.add_argument('--browser_args', nargs=1, type=str, help='Override ' | 31 parser.add_argument('--browser_args', nargs=1, type=str, help='Override ' |
| 28 'browser flags in code with these flags') | 32 'browser flags in code with these flags') |
| 29 parser.add_argument('--via_header_value', metavar='via_header', nargs=1, | 33 parser.add_argument('--via_header_value', metavar='via_header', nargs=1, |
| 30 default='1.1 Chrome-Compression-Proxy', help='What the via should match to ' | 34 default='1.1 Chrome-Compression-Proxy', help='What the via should match to ' |
| 31 'be considered valid') | 35 'be considered valid') |
| 32 parser.add_argument('--chrome_exec', nargs=1, type=str, help='The path to ' | 36 parser.add_argument('--chrome_exec', nargs=1, type=str, help='The path to ' |
| 33 'the Chrome or Chromium executable') | 37 'the Chrome or Chromium executable') |
| 34 parser.add_argument('chrome_driver', nargs=1, type=str, help='The path to ' | 38 parser.add_argument('chrome_driver', nargs=1, type=str, help='The path to ' |
| 35 'the ChromeDriver executable. If not given, the default system chrome ' | 39 'the ChromeDriver executable. If not given, the default system chrome ' |
| 36 'will be used.') | 40 'will be used.') |
| 37 # TODO(robertogden) make this a logging statement | 41 # TODO(robertogden): Log sys.argv here. |
| 38 print 'DEBUG: Args=', json.dumps(vars(parser.parse_args(sys.argv[1:]))) | |
| 39 return parser.parse_args(sys.argv[1:]) | 42 return parser.parse_args(sys.argv[1:]) |
| 40 | 43 |
| 41 def HandleException(test_name=None): | 44 def HandleException(test_name=None): |
| 42 """ | 45 """ |
| 43 Writes the exception being handled and a stack trace to stderr. | 46 Writes the exception being handled and a stack trace to stderr. |
| 47 | |
| 48 Args: | |
| 49 test_name: The string name of the test that led to this exception. | |
| 44 """ | 50 """ |
| 45 sys.stderr.write("**************************************\n") | 51 sys.stderr.write("**************************************\n") |
| 46 sys.stderr.write("**************************************\n") | 52 sys.stderr.write("**************************************\n") |
| 47 sys.stderr.write("** **\n") | 53 sys.stderr.write("** **\n") |
| 48 sys.stderr.write("** UNCAUGHT EXCEPTION **\n") | 54 sys.stderr.write("** UNCAUGHT EXCEPTION **\n") |
| 49 sys.stderr.write("** **\n") | 55 sys.stderr.write("** **\n") |
| 50 sys.stderr.write("**************************************\n") | 56 sys.stderr.write("**************************************\n") |
| 51 sys.stderr.write("**************************************\n") | 57 sys.stderr.write("**************************************\n") |
| 52 if test_name: | 58 if test_name: |
| 53 sys.stderr.write("Failed test: %s" % test_name) | 59 sys.stderr.write("Failed test: %s" % test_name) |
| 54 traceback.print_exception(*sys.exc_info()) | 60 traceback.print_exception(*sys.exc_info()) |
| 55 sys.exit(1) | 61 sys.exit(1) |
| 56 | 62 |
| 57 class TestDriver: | 63 class TestDriver: |
| 58 """ | 64 """ |
| 65 The main driver for an integration test. | |
| 66 | |
| 59 This class is the tool that is used by every integration test to interact with | 67 This class is the tool that is used by every integration test to interact with |
| 60 the Chromium browser and validate proper functionality. This class sits on top | 68 the Chromium browser and validate proper functionality. This class sits on top |
| 61 of the Selenium Chrome Webdriver with added utility and helper functions for | 69 of the Selenium Chrome Webdriver with added utility and helper functions for |
| 62 Chrome-Proxy. This class should be used with Python's 'with' operator. | 70 Chrome-Proxy. This class should be used with Python's 'with' operator. |
| 71 | |
| 72 Attributes: | |
| 73 _flags: A Namespace object from the call to ParseFlags() | |
| 74 _driver: A reference to the driver object from the Chrome Driver library. | |
| 75 _chrome_args: A set of string arguments to start Chrome with. | |
| 76 _url: The string URL that Chrome will navigate to for this test. | |
| 63 """ | 77 """ |
| 64 | 78 |
| 65 def __init__(self): | 79 def __init__(self): |
| 66 self._flags = ParseFlags() | 80 self._flags = ParseFlags() |
| 67 self._driver = None | 81 self._driver = None |
| 68 self._chrome_args = set() | 82 self._chrome_args = set() |
| 69 self._url = '' | 83 self._url = '' |
| 70 | 84 |
| 71 def __enter__(self): | 85 def __enter__(self): |
| 72 return self | 86 return self |
| 73 | 87 |
| 74 def __exit__(self, exc_type, exc_value, tb): | 88 def __exit__(self, exc_type, exc_value, tb): |
| 75 if self._driver: | 89 if self._driver: |
| 76 self._StopDriver() | 90 self._StopDriver() |
| 77 | 91 |
| 78 def _OverrideChromeArgs(self): | 92 def _OverrideChromeArgs(self): |
| 79 """ | 93 """ |
| 80 Overrides any given arguments in the code with those given on the command | 94 Overrides any given arguments in the code with those given on the command |
| 81 line. Arguments that need to be overridden may contain different values for | 95 line. |
| 96 | |
| 97 Arguments that need to be overridden may contain different values for | |
| 82 a flag given in the code. In that case, check by the flag whether to | 98 a flag given in the code. In that case, check by the flag whether to |
| 83 override the argument. | 99 override the argument. |
| 84 """ | 100 """ |
| 85 def GetDictKey(argument): | 101 def GetDictKey(argument): |
| 86 return argument.split('=', 1)[0] | 102 return argument.split('=', 1)[0] |
| 87 if self._flags.browser_args and len(self._flags.browser_args) > 0: | 103 if self._flags.browser_args and len(self._flags.browser_args) > 0: |
| 88 # Build a dict of flags mapped to the whole argument. | 104 # Build a dict of flags mapped to the whole argument. |
| 89 original_args = {} | 105 original_args = {} |
| 90 for arg in self._chrome_args: | 106 for arg in self._chrome_args: |
| 91 original_args[GetDictKey(arg)] = arg | 107 original_args[GetDictKey(arg)] = arg |
| 92 # Override by flag. | 108 # Override flags given in code with any command line arguments. |
| 93 for override_arg in shlex.split(self._flags.browser_args[0]): | 109 for override_arg in shlex.split(self._flags.browser_args[0]): |
| 94 arg_key = GetDictKey(override_arg) | 110 arg_key = GetDictKey(override_arg) |
| 95 if arg_key in original_args: | 111 if arg_key in original_args: |
| 96 self._chrome_args.remove(original_args[arg_key]) | 112 self._chrome_args.remove(original_args[arg_key]) |
| 97 self._chrome_args.add(override_arg) | 113 self._chrome_args.add(override_arg) |
| 98 | 114 |
| 99 def _StartDriver(self): | 115 def _StartDriver(self): |
| 100 """ | 116 """ |
| 101 Parses the flags to pass to Chromium, then starts the ChromeDriver. | 117 Parses the flags to pass to Chromium, then starts the ChromeDriver. |
| 102 """ | 118 """ |
| 103 self._OverrideChromeArgs() | 119 self._OverrideChromeArgs() |
| 104 options = Options() | 120 options = Options() |
| 105 for arg in self._chrome_args: | 121 for arg in self._chrome_args: |
| 106 options.add_argument(arg) | 122 options.add_argument(arg) |
| 107 capabilities = {'loggingPrefs': {'performance': 'INFO'}} | 123 capabilities = {'loggingPrefs': {'performance': 'INFO'}} |
| 108 if self._flags.chrome_exec: | 124 if self._flags.chrome_exec: |
| 109 capabilities['chrome.binary'] = self._flags.chrome_exec | 125 capabilities['chrome.binary'] = self._flags.chrome_exec |
| 110 driver = webdriver.Chrome(executable_path=self._flags.chrome_driver[0], | 126 driver = webdriver.Chrome(executable_path=self._flags.chrome_driver[0], |
| 111 chrome_options=options, desired_capabilities=capabilities) | 127 chrome_options=options, desired_capabilities=capabilities) |
| 112 driver.command_executor._commands.update({ | 128 driver.command_executor._commands.update({ |
| 113 'getAvailableLogTypes': ('GET', '/session/$sessionId/log/types'), | 129 'getAvailableLogTypes': ('GET', '/session/$sessionId/log/types'), |
| 114 'getLog': ('POST', '/session/$sessionId/log')}) | 130 'getLog': ('POST', '/session/$sessionId/log')}) |
| 115 self._driver = driver | 131 self._driver = driver |
| 116 | 132 |
| 117 def _StopDriver(self): | 133 def _StopDriver(self): |
| 118 """ | 134 """ |
| 119 Nicely stops the ChromeDriver. | 135 Nicely stops the ChromeDriver. |
| 120 """ | 136 """ |
| 121 self._driver.quit() | 137 self._driver.quit() |
| 122 del self._driver | 138 self._driver = None |
| 123 | 139 |
| 124 def AddChromeArgs(self, args): | 140 def AddChromeArgs(self, args): |
| 125 """ | 141 """ |
| 126 Adds multiple arguments that will be passed to Chromium at start. | 142 Adds multiple arguments that will be passed to Chromium at start. |
| 143 | |
| 144 Args: | |
| 145 args: An iterable of strings, each an argument to pass to Chrome at start. | |
| 127 """ | 146 """ |
| 128 for arg in args: | 147 for arg in args: |
| 129 self._chrome_args.add(arg) | 148 self._chrome_args.add(arg) |
| 130 | 149 |
| 131 def AddChromeArg(self, arg): | 150 def AddChromeArg(self, arg): |
| 132 """ | 151 """ |
| 133 Adds a single argument that will be passed to Chromium at start. | 152 Adds a single argument that will be passed to Chromium at start. |
| 153 | |
| 154 Args: | |
| 155 arg: a string argument to pass to Chrome at start | |
| 134 """ | 156 """ |
| 135 self._chrome_args.add(arg) | 157 self._chrome_args.add(arg) |
| 136 | 158 |
| 137 def RemoveChromeArgs(self, args): | 159 def RemoveChromeArgs(self, args): |
| 138 """ | 160 """ |
| 139 Removes multiple arguments that will no longer be passed to Chromium at | 161 Removes multiple arguments that will no longer be passed to Chromium at |
| 140 start. | 162 start. |
| 163 | |
| 164 Args: | |
| 165 args: An iterable of strings to no longer use the next time Chrome | |
| 166 starts. | |
| 141 """ | 167 """ |
| 142 for arg in args: | 168 for arg in args: |
| 143 self._chrome_args.discard(arg) | 169 self._chrome_args.discard(arg) |
| 144 | 170 |
| 145 def RemoveChromeArg(self, arg): | 171 def RemoveChromeArg(self, arg): |
| 146 """ | 172 """ |
| 147 Removes a single argument that will no longer be passed to Chromium at | 173 Removes a single argument that will no longer be passed to Chromium at |
| 148 start. | 174 start. |
| 175 | |
| 176 Args: | |
| 177 arg: A string flag to no longer use the next time Chrome starts. | |
| 149 """ | 178 """ |
| 150 self._chrome_args.discard(arg) | 179 self._chrome_args.discard(arg) |
| 151 | 180 |
| 152 def ClearChromeArgs(self): | 181 def ClearChromeArgs(self): |
| 153 """ | 182 """ |
| 154 Removes all arguments from Chromium at start. | 183 Removes all arguments from Chromium at start. |
| 155 """ | 184 """ |
| 156 self._chrome_args.clear() | 185 self._chrome_args.clear() |
| 157 | 186 |
| 158 def ClearCache(self): | 187 def ClearCache(self): |
| 159 """ | 188 """ |
| 160 Clears the browser cache. Important note: ChromeDriver automatically starts | 189 Clears the browser cache. |
| 190 | |
| 191 Important note: ChromeDriver automatically starts | |
| 161 a clean copy of Chrome on every instantiation. | 192 a clean copy of Chrome on every instantiation. |
| 162 """ | 193 """ |
| 163 self.ExecuteJavascript('if(window.chrome && chrome.benchmarking && ' | 194 self.ExecuteJavascript('if(window.chrome && chrome.benchmarking && ' |
| 164 'chrome.benchmarking.clearCache){chrome.benchmarking.clearCache(); ' | 195 'chrome.benchmarking.clearCache){chrome.benchmarking.clearCache(); ' |
| 165 'chrome.benchmarking.clearPredictorCache();chrome.benchmarking.' | 196 'chrome.benchmarking.clearPredictorCache();chrome.benchmarking.' |
| 166 'clearHostResolverCache();}') | 197 'clearHostResolverCache();}') |
| 167 | 198 |
| 168 def SetURL(self, url): | 199 def SetURL(self, url): |
| 169 """ | 200 """ |
| 170 Sets the URL that the browser will navigate to during the test. | 201 Sets the URL that the browser will navigate to during the test. |
| 202 | |
| 203 Args: | |
| 204 url: The string URL to navigate to | |
| 171 """ | 205 """ |
| 172 self._url = url | 206 self._url = url |
| 173 | 207 |
| 174 # TODO(robertogden) add timeout | 208 # TODO(robertogden): Add timeout. |
| 175 def LoadPage(self): | 209 def LoadPage(self): |
| 176 """ | 210 """ |
| 177 Starts Chromium with any arguments previously given and navigates to the | 211 Starts Chromium with any arguments previously given and navigates to the |
| 178 previously given URL. | 212 given URL. |
| 179 """ | 213 """ |
| 180 if not self._driver: | 214 if not self._driver: |
| 181 self._StartDriver() | 215 self._StartDriver() |
| 182 self._driver.get(self._url) | 216 self._driver.get(self._url) |
| 183 | 217 |
| 184 # TODO(robertogden) add timeout | 218 # TODO(robertogden): Add timeout. |
| 185 def ExecuteJavascript(self, script): | 219 def ExecuteJavascript(self, script): |
| 186 """ | 220 """ |
| 187 Executes the given javascript in the browser's current page as if it were on | 221 Executes the given javascript in the browser's current page as if it were on |
| 188 the console. Returns a string of whatever the evaluation was. | 222 the console. |
| 223 | |
| 224 Args: | |
| 225 script: A string of Javascript code. | |
| 226 Returns: | |
| 227 A string of the verbatim output from the Javascript execution. | |
| 189 """ | 228 """ |
| 190 if not self._driver: | 229 if not self._driver: |
| 191 self._StartDriver() | 230 self._StartDriver() |
| 192 return self._driver.execute_script("return " + script) | 231 return self._driver.execute_script("return " + script) |
| 193 | 232 |
| 233 def GetPerformanceLogs(self, method_filter=r'Network\.responseReceived'): | |
| 234 """ | |
| 235 Returns all logged Performance events from Chrome. | |
| 236 | |
| 237 Args: | |
| 238 method_filter: A regex expression to match the method of logged events | |
| 239 against. Only logs who's method matches the regex will be returned. | |
| 240 Returns: | |
| 241 Performance logs as a list of dicts, since the last time this function was | |
| 242 called. | |
| 243 """ | |
| 244 all_messages = [] | |
| 245 for log in self._driver.execute('getLog', {'type': 'performance'})['value']: | |
| 246 message = json.loads(log['message'])['message'] | |
| 247 if re.match(method_filter, message['method']): | |
| 248 all_messages.append(message) | |
| 249 return all_messages | |
| 250 | |
| 251 def GetHTTPResponses(self, include_favicon=False): | |
| 252 """ | |
| 253 Parses the Performance Logs and returns a list of HTTPResponse objects. | |
| 254 | |
| 255 This function should be called exactly once after every page load. | |
| 256 | |
| 257 Args: | |
| 258 include_favicon: A bool that if True will include responses for favicons. | |
| 259 Returns: | |
| 260 A list of HTTPResponse objects, each representing a single completed HTTP | |
| 261 transaction by Chrome. | |
| 262 """ | |
| 263 def MakeHTTPResponse(log_dict): | |
| 264 params = log_dict['params'] | |
| 265 response_dict = params['response'] | |
| 266 http_response_dict = { | |
| 267 'response_headers': response_dict['headers'], | |
| 268 'request_headers': response_dict['requestHeaders'], | |
| 269 'url': response_dict['url'], | |
| 270 'protocol': response_dict['requestHeaders'][':scheme'], | |
| 271 'status': response_dict['status'], | |
| 272 'request_type': params['type'] | |
| 273 } | |
| 274 return HTTPResponse(**http_response_dict) | |
| 275 all_responses = [] | |
| 276 for message in self.GetPerformanceLogs(): | |
| 277 response = MakeHTTPResponse(message) | |
| 278 is_favicon = response.url.endswith('favicon.ico') | |
| 279 if not is_favicon or include_favicon: | |
| 280 all_responses.append(response) | |
| 281 return all_responses | |
| 282 | |
| 283 class HTTPResponse: | |
| 284 """ | |
| 285 This class represents a single HTTP transaction (request and response) by | |
| 286 Chrome. | |
| 287 | |
| 288 This class also includes several convenience functions for ChromeProxy | |
| 289 specific assertions. | |
| 290 | |
| 291 Attributes: | |
| 292 _response_headers: A dict of response headers. | |
| 293 _request_headers: A dict of request headers. | |
| 294 _url: the fetched url | |
| 295 _protocol: The protocol used for the request. (HTTP, HTTPS, etc) | |
| 296 _status: The integer status code of the response | |
| 297 _request_type: What caused this request (Document, XHR, etc) | |
| 298 _flags: A Namespace object from ParseFlags() | |
| 299 """ | |
| 300 | |
| 301 def __init__(self, response_headers, request_headers, url, protocol, | |
| 302 status, request_type): | |
| 303 self._response_headers = response_headers | |
| 304 self._request_headers = request_headers | |
| 305 self._url = url | |
| 306 self._protocol = protocol | |
| 307 self._status = status | |
| 308 self._request_type = request_type | |
| 309 self._flags = ParseFlags() | |
| 310 | |
| 311 def __str__(self): | |
| 312 self_dict = { | |
| 313 'response_headers': self._response_headers, | |
| 314 'request_headers': self._request_headers, | |
| 315 'url': self._url, | |
| 316 'protocol': self._protocol, | |
| 317 'status': self._status, | |
| 318 'request_type': self._request_type | |
| 319 } | |
| 320 return json.dumps(self_dict) | |
| 321 | |
| 322 @property | |
| 323 def response_headers(self): | |
| 324 return self._response_headers | |
| 325 | |
| 326 @property | |
| 327 def request_headers(self): | |
| 328 return self._request_headers | |
| 329 | |
| 330 @property | |
| 331 def url(self): | |
| 332 return self._url | |
| 333 | |
| 334 @property | |
| 335 def protocol(self): | |
| 336 return self._protocol | |
| 337 | |
| 338 @property | |
| 339 def status(self): | |
| 340 return self._status | |
| 341 | |
| 342 @property | |
| 343 def request_type(self): | |
| 344 return self._request_type | |
| 345 | |
| 346 def UsedHTTPS(self): | |
| 347 return self._protocol == 'https' | |
|
sclittle
2016/12/06 18:11:30
As per offline discussion, it looks like this retu
Robert Ogden
2016/12/06 18:43:00
Done.
| |
| 348 | |
| 349 def ResponseHasViaHeader(self): | |
| 350 return 'via' in self._response_headers and (self._response_headers['via'] == | |
| 351 self._flags.via_header_value) | |
| 352 | |
| 353 def WasXHR(self): | |
| 354 return self.request_type == 'XHR' | |
| 194 | 355 |
| 195 class IntegrationTest: | 356 class IntegrationTest: |
| 196 """ | 357 """ |
| 197 A parent class for all integration tests with utility and assertion methods. | 358 A parent class for all integration tests with utility and assertion methods. |
| 359 | |
| 198 All methods starting with the word 'test' (ignoring case) will be called with | 360 All methods starting with the word 'test' (ignoring case) will be called with |
| 199 the RunAllTests() method which can be used in place of a main method. | 361 the RunAllTests() method which can be used in place of a main method. |
| 200 """ | 362 """ |
| 201 def RunAllTests(self): | 363 def RunAllTests(self): |
| 202 """ | 364 """ |
| 203 Runs all methods starting with the word 'test' (ignoring case) in the class. | 365 Runs all methods starting with the word 'test' (ignoring case) in the class. |
| 204 Can be used in place of a main method to run all tests in a class. | 366 Can be used in place of a main method to run all tests in a class. |
| 205 """ | 367 """ |
| 206 methodList = [method for method in dir(self) if callable(getattr(self, | 368 methodList = [method for method in dir(self) if callable(getattr(self, |
| 207 method)) and method.lower().startswith('test')] | 369 method)) and method.lower().startswith('test')] |
| 208 for method in methodList: | 370 for method in methodList: |
| 209 try: | 371 try: |
| 210 getattr(self, method)() | 372 getattr(self, method)() |
| 211 except Exception as e: | 373 except Exception as e: |
| 212 # Uses the Exception tuple from sys.exec_info() | 374 # Uses the Exception tuple from sys.exec_info(). |
| 213 HandleException(method) | 375 HandleException(method) |
| 214 | 376 |
| 215 # TODO(robertogden) add some nice assertion functions | 377 # TODO(robertogden): Add some nice assertion functions. |
| 216 | 378 |
| 217 def Fail(self, msg): | 379 def Fail(self, msg): |
| 380 """ | |
| 381 Called when a test fails an assertion. | |
| 382 | |
| 383 Args: | |
| 384 msg: The string message to print to stderr | |
| 385 """ | |
| 218 sys.stderr.write("**************************************\n") | 386 sys.stderr.write("**************************************\n") |
| 219 sys.stderr.write("**************************************\n") | 387 sys.stderr.write("**************************************\n") |
| 220 sys.stderr.write("** **\n") | 388 sys.stderr.write("** **\n") |
| 221 sys.stderr.write("** TEST FAILURE **\n") | 389 sys.stderr.write("** TEST FAILURE **\n") |
| 222 sys.stderr.write("** **\n") | 390 sys.stderr.write("** **\n") |
| 223 sys.stderr.write("**************************************\n") | 391 sys.stderr.write("**************************************\n") |
| 224 sys.stderr.write("**************************************\n") | 392 sys.stderr.write("**************************************\n") |
| 225 sys.stderr.write(msg, '\n') | 393 sys.stderr.write(msg, '\n') |
| 226 sys.exit(1) | 394 sys.exit(1) |
| OLD | NEW |