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