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

Side by Side Diff: tools/chrome_proxy/webdriver/common.py

Issue 2550433002: Add GetResponses() and related funcionality (Closed)
Patch Set: Better comments and doc. Simpler HTTPResponse init. Created 4 years 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 unified diff | Download patch
« no previous file with comments | « no previous file | tools/chrome_proxy/webdriver/simple_smoke.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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)
OLDNEW
« no previous file with comments | « no previous file | tools/chrome_proxy/webdriver/simple_smoke.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698