Chromium Code Reviews| Index: tools/chrome_proxy/webdriver/common.py |
| diff --git a/tools/chrome_proxy/webdriver/common.py b/tools/chrome_proxy/webdriver/common.py |
| index 29c7e58a6ccd1201162126d8092990618ea9b4f8..f508d23c117a1c9e4e280471185089535da09e73 100644 |
| --- a/tools/chrome_proxy/webdriver/common.py |
| +++ b/tools/chrome_proxy/webdriver/common.py |
| @@ -5,6 +5,7 @@ |
| import argparse |
| import json |
| import os |
| +import re |
| import shlex |
| import sys |
| import time |
| @@ -15,13 +16,16 @@ sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, |
| from selenium import webdriver |
| from selenium.webdriver.chrome.options import Options |
| -# TODO(robertogden) add logging |
| +# TODO(robertogden): Add logging. |
| def ParseFlags(): |
| """ |
| - Parses the given command line arguments and returns an object with the flags |
| - as properties. |
| + 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.
|
| + |
| + Returns: |
| + A new Namespace object with class properties for each argument added below. |
| + See pydoc for argparse. |
| """ |
| parser = argparse.ArgumentParser() |
| parser.add_argument('--browser_args', nargs=1, type=str, help='Override ' |
| @@ -34,13 +38,15 @@ def ParseFlags(): |
| parser.add_argument('chrome_driver', nargs=1, type=str, help='The path to ' |
| 'the ChromeDriver executable. If not given, the default system chrome ' |
| 'will be used.') |
| - # TODO(robertogden) make this a logging statement |
| - print 'DEBUG: Args=', json.dumps(vars(parser.parse_args(sys.argv[1:]))) |
| + # TODO(robertogden): Log sys.argv here. |
| return parser.parse_args(sys.argv[1:]) |
| def HandleException(test_name=None): |
| """ |
| Writes the exception being handled and a stack trace to stderr. |
| + |
| + Args: |
| + test_name: The string name of the test that led to this exception. |
| """ |
| sys.stderr.write("**************************************\n") |
| sys.stderr.write("**************************************\n") |
| @@ -56,10 +62,18 @@ def HandleException(test_name=None): |
| class TestDriver: |
| """ |
| + The main driver for an integration test. |
| + |
| This class is the tool that is used by every integration test to interact with |
| the Chromium browser and validate proper functionality. This class sits on top |
| of the Selenium Chrome Webdriver with added utility and helper functions for |
| Chrome-Proxy. This class should be used with Python's 'with' operator. |
| + |
| + Attributes: |
| + _flags: A Namespace object from the call to ParseFlags() |
| + _driver: A reference to the driver object from the Chrome Driver library. |
| + _chrome_args: A set of string arguments to start Chrome with. |
| + _url: The string URL that Chrome will navigate to for this test. |
| """ |
| def __init__(self): |
| @@ -78,7 +92,9 @@ class TestDriver: |
| def _OverrideChromeArgs(self): |
| """ |
| Overrides any given arguments in the code with those given on the command |
| - line. Arguments that need to be overridden may contain different values for |
| + line. |
| + |
| + Arguments that need to be overridden may contain different values for |
| a flag given in the code. In that case, check by the flag whether to |
| override the argument. |
| """ |
| @@ -89,7 +105,7 @@ class TestDriver: |
| original_args = {} |
| for arg in self._chrome_args: |
| original_args[GetDictKey(arg)] = arg |
| - # Override by flag. |
| + # Override flags given in code with any command line arguments. |
| for override_arg in shlex.split(self._flags.browser_args[0]): |
| arg_key = GetDictKey(override_arg) |
| if arg_key in original_args: |
| @@ -119,11 +135,14 @@ class TestDriver: |
| Nicely stops the ChromeDriver. |
| """ |
| self._driver.quit() |
| - del self._driver |
| + self._driver = None |
| def AddChromeArgs(self, args): |
| """ |
| Adds multiple arguments that will be passed to Chromium at start. |
| + |
| + Args: |
| + args: An iterable of strings, each an argument to pass to Chrome at start. |
| """ |
| for arg in args: |
| self._chrome_args.add(arg) |
| @@ -131,6 +150,9 @@ class TestDriver: |
| def AddChromeArg(self, arg): |
| """ |
| Adds a single argument that will be passed to Chromium at start. |
| + |
| + Args: |
| + arg: a string argument to pass to Chrome at start |
| """ |
| self._chrome_args.add(arg) |
| @@ -138,6 +160,10 @@ class TestDriver: |
| """ |
| Removes multiple arguments that will no longer be passed to Chromium at |
| start. |
| + |
| + Args: |
| + args: An iterable of strings to no longer use the next time Chrome |
| + starts. |
| """ |
| for arg in args: |
| self._chrome_args.discard(arg) |
| @@ -146,6 +172,9 @@ class TestDriver: |
| """ |
| Removes a single argument that will no longer be passed to Chromium at |
| start. |
| + |
| + Args: |
| + arg: A string flag to no longer use the next time Chrome starts. |
| """ |
| self._chrome_args.discard(arg) |
| @@ -157,7 +186,9 @@ class TestDriver: |
| def ClearCache(self): |
| """ |
| - Clears the browser cache. Important note: ChromeDriver automatically starts |
| + Clears the browser cache. |
| + |
| + Important note: ChromeDriver automatically starts |
| a clean copy of Chrome on every instantiation. |
| """ |
| self.ExecuteJavascript('if(window.chrome && chrome.benchmarking && ' |
| @@ -168,33 +199,164 @@ class TestDriver: |
| def SetURL(self, url): |
| """ |
| Sets the URL that the browser will navigate to during the test. |
| + |
| + Args: |
| + url: The string URL to navigate to |
| """ |
| self._url = url |
| - # TODO(robertogden) add timeout |
| + # TODO(robertogden): Add timeout. |
| def LoadPage(self): |
| """ |
| Starts Chromium with any arguments previously given and navigates to the |
| - previously given URL. |
| + given URL. |
| """ |
| if not self._driver: |
| self._StartDriver() |
| self._driver.get(self._url) |
| - # TODO(robertogden) add timeout |
| + # TODO(robertogden): Add timeout. |
| def ExecuteJavascript(self, script): |
| """ |
| Executes the given javascript in the browser's current page as if it were on |
| - the console. Returns a string of whatever the evaluation was. |
| + the console. |
| + |
| + Args: |
| + script: A string of Javascript code. |
| + Returns: |
| + A string of the verbatim output from the Javascript execution. |
| """ |
| if not self._driver: |
| self._StartDriver() |
| return self._driver.execute_script("return " + script) |
| + def GetPerformanceLogs(self, method_filter=r'Network\.responseReceived'): |
| + """ |
| + Returns all logged Performance events from Chrome. |
| + |
| + Args: |
| + method_filter: A regex expression to match the method of logged events |
| + against. Only logs who's method matches the regex will be returned. |
| + Returns: |
| + Performance logs as a list of dicts, since the last time this function was |
| + called. |
| + """ |
| + all_messages = [] |
| + for log in self._driver.execute('getLog', {'type': 'performance'})['value']: |
| + message = json.loads(log['message'])['message'] |
| + if re.match(method_filter, message['method']): |
| + all_messages.append(message) |
| + return all_messages |
| + |
| + def GetHTTPResponses(self, include_favicon=False): |
| + """ |
| + Parses the Performance Logs and returns a list of HTTPResponse objects. |
| + |
| + This function should be called exactly once after every page load. |
| + |
| + Args: |
| + include_favicon: A bool that if True will include responses for favicons. |
| + Returns: |
| + A list of HTTPResponse objects, each representing a single completed HTTP |
| + transaction by Chrome. |
| + """ |
| + def MakeHTTPResponse(log_dict): |
| + params = log_dict['params'] |
| + response_dict = params['response'] |
| + http_response_dict = { |
| + 'response_headers': response_dict['headers'], |
| + 'request_headers': response_dict['requestHeaders'], |
| + 'url': response_dict['url'], |
| + 'protocol': response_dict['requestHeaders'][':scheme'], |
| + 'status': response_dict['status'], |
| + 'request_type': params['type'] |
| + } |
| + return HTTPResponse(**http_response_dict) |
| + all_responses = [] |
| + for message in self.GetPerformanceLogs(): |
| + response = MakeHTTPResponse(message) |
| + is_favicon = response.url.endswith('favicon.ico') |
| + if not is_favicon or include_favicon: |
| + all_responses.append(response) |
| + return all_responses |
| + |
| +class HTTPResponse: |
| + """ |
| + This class represents a single HTTP transaction (request and response) by |
| + Chrome. |
| + |
| + This class also includes several convenience functions for ChromeProxy |
| + specific assertions. |
| + |
| + Attributes: |
| + _response_headers: A dict of response headers. |
| + _request_headers: A dict of request headers. |
| + _url: the fetched url |
| + _protocol: The protocol used for the request. (HTTP, HTTPS, etc) |
| + _status: The integer status code of the response |
| + _request_type: What caused this request (Document, XHR, etc) |
| + _flags: A Namespace object from ParseFlags() |
| + """ |
| + |
| + def __init__(self, response_headers, request_headers, url, protocol, |
| + status, request_type): |
| + self._response_headers = response_headers |
| + self._request_headers = request_headers |
| + self._url = url |
| + self._protocol = protocol |
| + self._status = status |
| + self._request_type = request_type |
| + self._flags = ParseFlags() |
| + |
| + def __str__(self): |
| + self_dict = { |
| + 'response_headers': self._response_headers, |
| + 'request_headers': self._request_headers, |
| + 'url': self._url, |
| + 'protocol': self._protocol, |
| + 'status': self._status, |
| + 'request_type': self._request_type |
| + } |
| + return json.dumps(self_dict) |
| + |
| + @property |
| + def response_headers(self): |
| + return self._response_headers |
| + |
| + @property |
| + def request_headers(self): |
| + return self._request_headers |
| + |
| + @property |
| + def url(self): |
| + return self._url |
| + |
| + @property |
| + def protocol(self): |
| + return self._protocol |
| + |
| + @property |
| + def status(self): |
| + return self._status |
| + |
| + @property |
| + def request_type(self): |
| + return self._request_type |
| + |
| + def UsedHTTPS(self): |
| + 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.
|
| + |
| + def ResponseHasViaHeader(self): |
| + return 'via' in self._response_headers and (self._response_headers['via'] == |
| + self._flags.via_header_value) |
| + |
| + def WasXHR(self): |
| + return self.request_type == 'XHR' |
| class IntegrationTest: |
| """ |
| A parent class for all integration tests with utility and assertion methods. |
| + |
| All methods starting with the word 'test' (ignoring case) will be called with |
| the RunAllTests() method which can be used in place of a main method. |
| """ |
| @@ -209,12 +371,18 @@ class IntegrationTest: |
| try: |
| getattr(self, method)() |
| except Exception as e: |
| - # Uses the Exception tuple from sys.exec_info() |
| + # Uses the Exception tuple from sys.exec_info(). |
| HandleException(method) |
| - # TODO(robertogden) add some nice assertion functions |
| + # TODO(robertogden): Add some nice assertion functions. |
| def Fail(self, msg): |
| + """ |
| + Called when a test fails an assertion. |
| + |
| + Args: |
| + msg: The string message to print to stderr |
| + """ |
| sys.stderr.write("**************************************\n") |
| sys.stderr.write("**************************************\n") |
| sys.stderr.write("** **\n") |