OLD | NEW |
| (Empty) |
1 #!/usr/bin/python | |
2 # | |
3 # -*- coding: utf-8 -*- | |
4 # vim: set ts=4 sw=4 et sts=4 ai: | |
5 | |
6 import atexit | |
7 import base64 | |
8 import cStringIO as StringIO | |
9 import getpass | |
10 import httplib | |
11 import json as simplejson | |
12 import os | |
13 import platform | |
14 import pprint | |
15 import re | |
16 import socket | |
17 import sys | |
18 import time | |
19 import urllib2 | |
20 import zipfile | |
21 | |
22 import argparse | |
23 parser = argparse.ArgumentParser() | |
24 | |
25 parser.add_argument( | |
26 "-b", "--browser", type=str, required=True, | |
27 choices=['Firefox', 'Chrome', 'Ie', 'PhantomJS', 'Remote'], | |
28 help="Which WebDriver to use.") | |
29 | |
30 parser.add_argument( | |
31 "-f", "--flag", action='append', default=[], | |
32 help="Command line flags to pass to the browser, " | |
33 "currently only available for Chrome. " | |
34 "Each flag must be a separate --flag invoccation.") | |
35 | |
36 parser.add_argument( | |
37 "-x", "--virtual", action='store_true', default=False, | |
38 help="Use a virtual screen system such as Xvfb, Xephyr or Xvnc.") | |
39 | |
40 parser.add_argument( | |
41 "-d", "--dontexit", action='store_true', default=False, | |
42 help="At end of testing, don't exit.") | |
43 | |
44 parser.add_argument( | |
45 "-v", "--verbose", action='store_true', default=False, | |
46 help="Output more information.") | |
47 | |
48 parser.add_argument( | |
49 "-u", "--upload", action='store_true', default=False, | |
50 help="Upload images to picture sharing site (http://postimage.org/)," | |
51 " only really useful for testbots.") | |
52 | |
53 # Only used by the Remote browser option. | |
54 parser.add_argument( | |
55 "--remote-executor", type=str, | |
56 help="Location of the Remote executor.") | |
57 | |
58 parser.add_argument( | |
59 "--remote-caps", action='append', | |
60 help="Location of capabilities to request on Remote executor.", | |
61 default=[]) | |
62 | |
63 parser.add_argument( | |
64 "-s", "--sauce", action='store_true', default=False, | |
65 help="Use the SauceLab's Selenium farm rather then locally starting" | |
66 " selenium. SAUCE_USERNAME and SAUCE_ACCESS_KEY must be set in" | |
67 " environment.") | |
68 | |
69 # Subunit / testrepository support | |
70 parser.add_argument( | |
71 "--subunit", action='store_true', default=False, | |
72 help="Output raw subunit binary data.") | |
73 | |
74 parser.add_argument( | |
75 "--list", action='store_true', default=False, | |
76 help="List tests which are available.") | |
77 | |
78 parser.add_argument( | |
79 "--load-list", type=argparse.FileType('r'), | |
80 help="List of tests to run.") | |
81 | |
82 args = parser.parse_args() | |
83 | |
84 if args.verbose and args.subunit: | |
85 raise SystemExit("--verbose and --subunit are not compatible.") | |
86 | |
87 # Make sure the repository is setup and the dependencies exist | |
88 # ----------------------------------------------------------------------------- | |
89 | |
90 import subprocess | |
91 | |
92 caps = {} | |
93 if not args.sauce: | |
94 # Get any selenium drivers we might need | |
95 if args.browser == "Chrome": | |
96 # Get ChromeDriver if it's not in the path... | |
97 # https://code.google.com/p/chromedriver/downloads/list | |
98 chromedriver_bin = "chromedriver" | |
99 chromedriver_url_tmpl = "http://chromedriver.storage.googleapis.com/2.6/
chromedriver_%s%s.zip" # noqa | |
100 | |
101 if platform.system() == "Linux": | |
102 if platform.processor() == "x86_64": | |
103 # 64 bit binary needed | |
104 chromedriver_url = chromedriver_url_tmpl % ("linux", "64") | |
105 else: | |
106 # 32 bit binary needed | |
107 chromedriver_url = chromedriver_url_tmpl % ("linux", "32") | |
108 | |
109 elif platform.system() == "Darwin": | |
110 chromedriver_url = chromedriver_url_tmpl % ("mac", "32") | |
111 elif platform.system() == "win32": | |
112 chromedriver_url = chromedriver_url_tmpl % ("win", "32") | |
113 chromedriver_url = "chromedriver.exe" | |
114 | |
115 try: | |
116 if subprocess.call(chromedriver_bin) != 0: | |
117 raise OSError("Return code?") | |
118 except OSError: | |
119 chromedriver_local = os.path.join("tools", chromedriver_bin) | |
120 | |
121 if not os.path.exists(chromedriver_local): | |
122 datafile = StringIO.StringIO( | |
123 urllib2.urlopen(chromedriver_url).read()) | |
124 contents = zipfile.ZipFile(datafile, 'r') | |
125 contents.extract(chromedriver_bin, "tools") | |
126 | |
127 chromedriver = os.path.realpath(chromedriver_local) | |
128 os.chmod(chromedriver, 0755) | |
129 else: | |
130 chromedriver = "chromedriver" | |
131 | |
132 elif args.browser == "Firefox": | |
133 pass | |
134 | |
135 elif args.browser == "PhantomJS": | |
136 phantomjs_bin = None | |
137 if platform.system() == "Linux": | |
138 phantomjs_bin = "phantomjs" | |
139 if platform.processor() == "x86_64": | |
140 # 64 bit binary needed | |
141 phantomjs_url = "https://phantomjs.googlecode.com/files/phantomj
s-1.9.0-linux-x86_64.tar.bz2" # noqa | |
142 else: | |
143 # 32 bit binary needed | |
144 phantomjs_url = "https://phantomjs.googlecode.com/files/phantomj
s-1.9.0-linux-i686.tar.bz2" # noqa | |
145 | |
146 phantomjs_local = os.path.join("tools", phantomjs_bin) | |
147 if not os.path.exists(phantomjs_local): | |
148 datafile = StringIO.StringIO( | |
149 urllib2.urlopen(phantomjs_url).read()) | |
150 contents = tarfile.TarFile.open(fileobj=datafile, mode='r:bz2') | |
151 file("tools/"+phantomjs_bin, "w").write( | |
152 contents.extractfile( | |
153 "phantomjs-1.9.0-linux-x86_64/bin/"+phantomjs_bin | |
154 ).read()) | |
155 | |
156 phantomjs = os.path.realpath(phantomjs_local) | |
157 os.chmod(phantomjs, 0755) | |
158 else: | |
159 if platform.system() == "Darwin": | |
160 phantomjs_url = "https://phantomjs.googlecode.com/files/phantomj
s-1.9.0-macosx.zip" # noqa | |
161 phantomjs_bin = "phantomjs" | |
162 | |
163 elif platform.system() == "win32": | |
164 chromedriver_bin = "https://phantomjs.googlecode.com/files/phant
omjs-1.9.0-windows.zip" # noqa | |
165 phantomjs_url = "phantomjs.exe" | |
166 | |
167 phantomjs_local = os.path.join("tools", phantomjs_bin) | |
168 if not os.path.exists(phantomjs_local): | |
169 datafile = StringIO.StringIO( | |
170 urllib2.urlopen(phantomjs_url).read()) | |
171 contents = zipfile.ZipFile(datafile, 'r') | |
172 contents.extract(phantomjs_bin, "tools") | |
173 | |
174 phantomjs = os.path.realpath(phantomjs_local) | |
175 os.chmod(phantomjs, 0755) | |
176 else: | |
177 assert os.environ['SAUCE_USERNAME'] | |
178 assert os.environ['SAUCE_ACCESS_KEY'] | |
179 sauce_username = os.environ['SAUCE_USERNAME'] | |
180 sauce_access_key = os.environ['SAUCE_ACCESS_KEY'] | |
181 | |
182 # Download the Sauce Connect script | |
183 sauce_connect_url = "http://saucelabs.com/downloads/Sauce-Connect-latest.zip
" # noqa | |
184 sauce_connect_bin = "Sauce-Connect.jar" | |
185 sauce_connect_local = os.path.join("tools", sauce_connect_bin) | |
186 if not os.path.exists(sauce_connect_local): | |
187 datafile = StringIO.StringIO(urllib2.urlopen(sauce_connect_url).read()) | |
188 contents = zipfile.ZipFile(datafile, 'r') | |
189 contents.extract(sauce_connect_bin, "tools") | |
190 | |
191 if 'TRAVIS_JOB_NUMBER' in os.environ: | |
192 tunnel_id = os.environ['TRAVIS_JOB_NUMBER'] | |
193 else: | |
194 tunnel_id = "%s:%s" % (socket.gethostname(), os.getpid()) | |
195 args.remote_caps.append('tunnel-identifier=%s' % tunnel_id) | |
196 | |
197 # Kill the tunnel when we die | |
198 def kill_tunnel(sauce_tunnel): | |
199 if sauce_tunnel.returncode is None: | |
200 sauce_tunnel.terminate() | |
201 | |
202 timeout = time.time() | |
203 while sauce_tunnel.poll() is None: | |
204 if time.time() - timeout < 30: | |
205 time.sleep(1) | |
206 else: | |
207 sauce_tunnel.kill() | |
208 | |
209 readyfile = "."+tunnel_id | |
210 sauce_tunnel = None | |
211 try: | |
212 sauce_log = file("sauce_tunnel.log", "w") | |
213 sauce_tunnel = subprocess.Popen( | |
214 ["java", "-jar", sauce_connect_local, | |
215 "--readyfile", readyfile, | |
216 "--tunnel-identifier", tunnel_id, | |
217 sauce_username, sauce_access_key], | |
218 stdout=sauce_log, stderr=sauce_log) | |
219 | |
220 atexit.register(kill_tunnel, sauce_tunnel) | |
221 | |
222 # Wait for the tunnel to come up | |
223 while not os.path.exists(readyfile): | |
224 time.sleep(0.5) | |
225 | |
226 except: | |
227 if sauce_tunnel: | |
228 kill_tunnel(sauce_tunnel) | |
229 raise | |
230 | |
231 args.remote_executor = "http://%s:%s@localhost:4445/wd/hub" % ( | |
232 sauce_username, sauce_access_key) | |
233 | |
234 custom_data = {} | |
235 git_info = subprocess.Popen( | |
236 ["git", "describe", "--all", "--long"], stdout=subprocess.PIPE | |
237 ).communicate()[0] | |
238 custom_data["git-info"] = git_info | |
239 | |
240 git_commit = subprocess.Popen( | |
241 ["git", "rev-parse", "HEAD"], stdout=subprocess.PIPE | |
242 ).communicate()[0] | |
243 custom_data["git-commit"] = git_commit | |
244 | |
245 caps['tags'] = [] | |
246 if 'TRAVIS_BUILD_NUMBER' in os.environ: | |
247 # Send travis information upstream | |
248 caps['build'] = "%s %s" % ( | |
249 os.environ['TRAVIS_REPO_SLUG'], | |
250 os.environ['TRAVIS_BUILD_NUMBER'], | |
251 ) | |
252 caps['name'] = "Travis run for %s" % os.environ['TRAVIS_REPO_SLUG'] | |
253 | |
254 caps['tags'].append( | |
255 "repo=%s" % os.environ['TRAVIS_REPO_SLUG']) | |
256 caps['tags'].append( | |
257 "branch=%s" % os.environ['TRAVIS_BRANCH']) | |
258 | |
259 travis_env = [ | |
260 'TRAVIS_BRANCH', | |
261 'TRAVIS_BUILD_ID', | |
262 'TRAVIS_BUILD_NUMBER', | |
263 'TRAVIS_COMMIT', | |
264 'TRAVIS_COMMIT_RANGE', | |
265 'TRAVIS_JOB_ID', | |
266 'TRAVIS_JOB_NUMBER', | |
267 'TRAVIS_PULL_REQUEST', | |
268 'TRAVIS_REPO_SLUG', | |
269 ] | |
270 | |
271 for env in travis_env: | |
272 tag = env[len('TRAVIS_'):].lower() | |
273 value = os.environ.get(env, None) | |
274 if not value: | |
275 continue | |
276 custom_data[tag] = value | |
277 | |
278 custom_data["github-url"] = ( | |
279 "https://github.com/%s/tree/%s" % ( | |
280 os.environ['TRAVIS_REPO_SLUG'], git_commit)) | |
281 custom_data["travis-url"] = ( | |
282 "https://travis-ci.org/%s/builds/%s" % ( | |
283 os.environ['TRAVIS_REPO_SLUG'], | |
284 os.environ['TRAVIS_BUILD_ID'])) | |
285 else: | |
286 # Collect information about who/what is running the test | |
287 caps['name'] = "Manual run for %s" % getpass.getuser() | |
288 caps['build'] = git_info | |
289 | |
290 caps['tags'].append('user=%s' % getpass.getuser()) | |
291 caps['tags'].append('host=%s' % socket.gethostname()) | |
292 | |
293 # ----------------------------------------------------------------------------- | |
294 | |
295 import subunit | |
296 import testtools | |
297 | |
298 if args.list: | |
299 data = file("test/testcases.js").read() | |
300 for test in re.compile("(?<=').+(?=')").findall(data): | |
301 print test[:-5] | |
302 sys.exit(-1) | |
303 | |
304 if args.load_list: | |
305 tests = list(set(x.split(':')[0].strip()+'.html' | |
306 for x in args.load_list.readlines())) | |
307 else: | |
308 tests = [] | |
309 | |
310 # Collect summary of all the individual test runs | |
311 summary = testtools.StreamSummary() | |
312 | |
313 # Output information to stdout | |
314 if not args.subunit: | |
315 # Output test failures | |
316 result_streams = [testtools.TextTestResult(sys.stdout)] | |
317 if args.verbose: | |
318 import unittest | |
319 # Output individual test progress | |
320 result_streams.insert(0, | |
321 unittest.TextTestResult( | |
322 unittest.runner._WritelnDecorator(sys.stdout), False, 2)) | |
323 # Human readable test output | |
324 pertest = testtools.StreamToExtendedDecorator( | |
325 testtools.MultiTestResult(*result_streams)) | |
326 else: | |
327 from subunit.v2 import StreamResultToBytes | |
328 pertest = StreamResultToBytes(sys.stdout) | |
329 | |
330 if args.list: | |
331 output = subunit.CopyStreamResult([summary, pertest]) | |
332 output.startTestRun() | |
333 for test in re.compile("(?<=').+(?=')").findall( | |
334 file("test/testcases.js").read()): | |
335 output.status(test_status='exists', test_id=test[:-5]) | |
336 | |
337 output.stopTestRun() | |
338 sys.exit(-1) | |
339 | |
340 output = subunit.CopyStreamResult([summary, pertest]) | |
341 output.startTestRun() | |
342 | |
343 # Start up a local HTTP server which serves the files to the browser and | |
344 # collects the test results. | |
345 # ----------------------------------------------------------------------------- | |
346 import SimpleHTTPServer | |
347 import SocketServer | |
348 import threading | |
349 import cgi | |
350 import re | |
351 | |
352 import itertools | |
353 import mimetools | |
354 import mimetypes | |
355 | |
356 | |
357 class MultiPartForm(object): | |
358 """Accumulate the data to be used when posting a form.""" | |
359 | |
360 def __init__(self): | |
361 self.form_fields = [] | |
362 self.files = [] | |
363 self.boundary = mimetools.choose_boundary() | |
364 return | |
365 | |
366 def get_content_type(self): | |
367 return 'multipart/form-data; boundary=%s' % self.boundary | |
368 | |
369 def add_field(self, name, value): | |
370 """Add a simple field to the form data.""" | |
371 self.form_fields.append((name, value)) | |
372 return | |
373 | |
374 def add_file(self, fieldname, filename, fileHandle, mimetype=None): | |
375 """Add a file to be uploaded.""" | |
376 body = fileHandle.read() | |
377 if mimetype is None: | |
378 mimetype = ( | |
379 mimetypes.guess_type(filename)[0] or | |
380 'application/octet-stream') | |
381 self.files.append((fieldname, filename, mimetype, body)) | |
382 return | |
383 | |
384 def __str__(self): | |
385 """Return a string representing the form data, with attached files.""" | |
386 # Build a list of lists, each containing "lines" of the | |
387 # request. Each part is separated by a boundary string. | |
388 # Once the list is built, return a string where each | |
389 # line is separated by '\r\n'. | |
390 parts = [] | |
391 part_boundary = '--' + self.boundary | |
392 | |
393 # Add the form fields | |
394 parts.extend([ | |
395 part_boundary, | |
396 'Content-Disposition: form-data; name="%s"' % name, | |
397 '', | |
398 value, | |
399 ] for name, value in self.form_fields) | |
400 | |
401 # Add the files to upload | |
402 parts.extend([ | |
403 part_boundary, | |
404 'Content-Disposition: file; name="%s"; filename="%s"' % ( | |
405 field_name, filename), | |
406 'Content-Type: %s' % content_type, | |
407 '', | |
408 body, | |
409 ] for field_name, filename, content_type, body in self.files) | |
410 | |
411 # Flatten the list and add closing boundary marker, | |
412 # then return CR+LF separated data | |
413 flattened = list(str(b) for b in itertools.chain(*parts)) | |
414 flattened.append('--' + self.boundary + '--') | |
415 flattened.append('') | |
416 return '\r\n'.join(flattened) | |
417 | |
418 | |
419 critical_failure = False | |
420 | |
421 | |
422 class ServerHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): | |
423 STATUS = {0: 'success', 1: 'fail', 2: 'fail', 3: 'skip'} | |
424 | |
425 # Make the HTTP requests be quiet | |
426 def log_message(self, format, *a): | |
427 if args.verbose: | |
428 SimpleHTTPServer.SimpleHTTPRequestHandler.log_message( | |
429 self, format, *a) | |
430 | |
431 def do_POST(self): | |
432 global critical_failure | |
433 already_failed = critical_failure | |
434 | |
435 form = cgi.FieldStorage( | |
436 fp=self.rfile, | |
437 headers=self.headers, | |
438 environ={ | |
439 'REQUEST_METHOD': 'POST', | |
440 'CONTENT_TYPE': self.headers['Content-Type'], | |
441 }) | |
442 | |
443 overall_status = 0 | |
444 test_id = None | |
445 try: | |
446 json_data = form.getvalue('data') | |
447 data = simplejson.loads(json_data) | |
448 except ValueError, e: | |
449 critical_failure = True | |
450 | |
451 test_id = "CRITICAL-FAILURE" | |
452 | |
453 msg = "Unable to decode JSON object (%s)\n%s" % (e, json_data) | |
454 overall_status = 1 | |
455 output.status( | |
456 test_id="CRITICAL-FAILURE", | |
457 test_status='fail', | |
458 test_tags=[args.browser], | |
459 file_name='traceback', | |
460 file_bytes=msg, | |
461 mime_type='text/plain; charset=UTF-8', | |
462 eof=True) | |
463 else: | |
464 test_id = data['testName'][:-5] | |
465 for result in data['results']: | |
466 info = dict(result) | |
467 info.pop('_structured_clone', None) | |
468 | |
469 if not isinstance(result['message'], (str, unicode)): | |
470 msg = str(result['message']) | |
471 else: | |
472 msg = result['message'] | |
473 | |
474 overall_status += result['status'] | |
475 output.status( | |
476 test_id="%s:%s" % (test_id, result['name']), | |
477 test_status=self.STATUS[result['status']], | |
478 test_tags=[args.browser], | |
479 file_name='traceback', | |
480 file_bytes=msg, | |
481 mime_type='text/plain; charset=UTF-8', | |
482 eof=True) | |
483 | |
484 if args.verbose and 'debug' in data and overall_status > 0: | |
485 output.status( | |
486 test_id="%s:debug-log" % (test_id), | |
487 test_status='fail', | |
488 test_tags=[args.browser], | |
489 file_name='traceback', | |
490 file_bytes=data['debug'], | |
491 mime_type='text/plain; charset=UTF-8', | |
492 eof=True) | |
493 | |
494 # Take a screenshot of result if a failure occurred. | |
495 if overall_status > 0 and (args.virtual or args.browser == "Remote"): | |
496 time.sleep(1) | |
497 | |
498 try: | |
499 screenshot = test_id + '.png' | |
500 if args.virtual: | |
501 disp.grab().save(screenshot) | |
502 elif args.browser == "Remote": | |
503 global browser | |
504 browser.save_screenshot(screenshot) | |
505 | |
506 # On android we want to do a | |
507 # adb run /system/bin/screencap -p /sdcard/FILENAME.png | |
508 # adb cp FILENAME.png .... | |
509 | |
510 if args.upload and not already_failed: | |
511 form = MultiPartForm() | |
512 form.add_field('adult', 'no') | |
513 form.add_field('optsize', '0') | |
514 form.add_file( | |
515 'upload[]', screenshot, fileHandle=open(screenshot, 'rb'
)) | |
516 | |
517 request = urllib2.Request("http://postimage.org/") | |
518 body = str(form) | |
519 request.add_header('Content-type', form.get_content_type()) | |
520 request.add_header('Content-length', len(body)) | |
521 request.add_data(body) | |
522 | |
523 result = urllib2.urlopen(request).read() | |
524 print "Screenshot at:", re.findall("""<td><textarea wrap='of
f' onmouseover='this.focus\(\)' onfocus='this.select\(\)' id="code_1" scrolling=
"no">([^<]*)</textarea></td>""", result) # noqa | |
525 except Exception, e: | |
526 print e | |
527 | |
528 response = "OK" | |
529 self.send_response(200) | |
530 self.send_header("Content-type", "text/plain") | |
531 self.send_header("Content-length", len(response)) | |
532 self.end_headers() | |
533 self.wfile.write(response) | |
534 self.wfile.close() | |
535 | |
536 if args.sauce: | |
537 port = 55001 | |
538 else: | |
539 port = 0 # Bind to any port on localhost | |
540 | |
541 while True: | |
542 try: | |
543 httpd = SocketServer.TCPServer( | |
544 ("127.0.0.1", port), | |
545 ServerHandler) | |
546 break | |
547 except socket.error as e: | |
548 print e | |
549 time.sleep(5) | |
550 | |
551 port = httpd.socket.getsockname()[-1] | |
552 print "Serving at", port | |
553 | |
554 httpd_thread = threading.Thread(target=httpd.serve_forever) | |
555 httpd_thread.daemon = True | |
556 httpd_thread.start() | |
557 | |
558 | |
559 # Start up a virtual display, useful for testing on headless servers. | |
560 # ----------------------------------------------------------------------------- | |
561 | |
562 VIRTUAL_SIZE = (1024, 2000) | |
563 | |
564 # PhantomJS doesn't need a display | |
565 disp = None | |
566 if args.virtual and args.browser != "PhantomJS": | |
567 from pyvirtualdisplay.smartdisplay import SmartDisplay | |
568 | |
569 try: | |
570 disp = SmartDisplay( | |
571 visible=0, bgcolor='black', size=VIRTUAL_SIZE).start() | |
572 atexit.register(disp.stop) | |
573 except: | |
574 if disp: | |
575 disp.stop() | |
576 raise | |
577 | |
578 | |
579 # Start up the web browser and run the tests. | |
580 # ---------------------------------------------------------------------------- | |
581 | |
582 from selenium import webdriver | |
583 from selenium.common import exceptions as selenium_exceptions | |
584 from selenium.webdriver.common.keys import Keys as selenium_keys | |
585 | |
586 driver_arguments = {} | |
587 if args.browser == "Chrome": | |
588 import tempfile | |
589 import shutil | |
590 | |
591 # We reference shutil to make sure it isn't garbaged collected before we | |
592 # use it. | |
593 def directory_cleanup(directory, shutil=shutil): | |
594 try: | |
595 shutil.rmtree(directory) | |
596 except OSError, e: | |
597 pass | |
598 | |
599 try: | |
600 user_data_dir = tempfile.mkdtemp() | |
601 atexit.register(directory_cleanup, user_data_dir) | |
602 except: | |
603 directory_cleanup(user_data_dir) | |
604 raise | |
605 | |
606 driver_arguments['chrome_options'] = webdriver.ChromeOptions() | |
607 # Make printable | |
608 webdriver.ChromeOptions.__repr__ = lambda self: str(self.__dict__) | |
609 chrome_flags = [ | |
610 '--user-data-dir=%s' % user_data_dir, | |
611 '--enable-logging', | |
612 '--start-maximized', | |
613 '--disable-default-apps', | |
614 '--disable-extensions', | |
615 '--disable-plugins', | |
616 ] | |
617 chrome_flags += args.flag | |
618 # Travis-CI uses OpenVZ containers which are incompatible with the sandbox | |
619 # technology. | |
620 # See https://code.google.com/p/chromium/issues/detail?id=31077 for more | |
621 # information. | |
622 if 'TRAVIS' in os.environ: | |
623 chrome_flags += [ | |
624 '--no-sandbox', | |
625 '--disable-setuid-sandbox', | |
626 '--allow-sandbox-debugging', | |
627 ] | |
628 for flag in chrome_flags: | |
629 driver_arguments['chrome_options'].add_argument(flag) | |
630 | |
631 #driver_arguments['chrome_options'].binary_location = ( | |
632 # '/usr/bin/google-chrome') | |
633 driver_arguments['executable_path'] = chromedriver | |
634 | |
635 | |
636 elif args.browser == "Firefox": | |
637 driver_arguments['firefox_profile'] = webdriver.FirefoxProfile() | |
638 # Firefox will often pop-up a dialog saying "script is taking too long" or | |
639 # similar. So we can notice this problem we use "accept" rather then the | |
640 # default "dismiss". | |
641 webdriver.DesiredCapabilities.FIREFOX[ | |
642 "unexpectedAlertBehaviour"] = "accept" | |
643 | |
644 elif args.browser == "PhantomJS": | |
645 driver_arguments['executable_path'] = phantomjs | |
646 driver_arguments['service_args'] = ['--remote-debugger-port=9000'] | |
647 | |
648 elif args.browser == "Remote": | |
649 driver_arguments['command_executor'] = args.remote_executor | |
650 | |
651 for arg in args.remote_caps: | |
652 if not arg.strip(): | |
653 continue | |
654 | |
655 if arg.find('=') < 0: | |
656 caps.update(getattr( | |
657 webdriver.DesiredCapabilities, arg.strip().upper())) | |
658 else: | |
659 bits = arg.split('=') | |
660 base = caps | |
661 for arg in bits[:-2]: | |
662 if arg not in base: | |
663 base[arg] = {} | |
664 base = base[arg] | |
665 base[bits[-2]] = bits[-1] | |
666 driver_arguments['desired_capabilities'] = caps | |
667 | |
668 major_failure = False | |
669 browser = None | |
670 session_id = None | |
671 try: | |
672 try: | |
673 if args.verbose: | |
674 print driver_arguments | |
675 browser = getattr(webdriver, args.browser)(**driver_arguments) | |
676 session_id = browser.session_id | |
677 atexit.register(browser.quit) | |
678 except: | |
679 if browser: | |
680 browser.quit() | |
681 raise | |
682 | |
683 # Load an empty page so the body element is always visible | |
684 browser.get('data:text/html;charset=utf-8,<!DOCTYPE html><html><body>EMPTY</
body></html>') # noqa | |
685 if args.virtual and args.browser == "Firefox": | |
686 # Calling browser.maximize_window() doesn't work as we don't have a | |
687 # window manager, so instead we for the size/position. | |
688 browser.set_window_position(0, 0) | |
689 browser.set_window_size(*VIRTUAL_SIZE) | |
690 # Also lets go into full screen mode to get rid of the "Chrome" around | |
691 # the edges. | |
692 e = browser.find_element_by_tag_name('body') | |
693 e.send_keys(selenium_keys.F11) | |
694 | |
695 url = 'http://localhost:%i/test/test-runner.html?%s' % ( | |
696 port, "|".join(tests)) | |
697 browser.get(url) | |
698 | |
699 def close_other_windows(browser, url): | |
700 for win in browser.window_handles: | |
701 browser.switch_to_window(win) | |
702 if browser.current_url != url: | |
703 browser.close() | |
704 browser.switch_to_window(browser.window_handles[0]) | |
705 | |
706 while True: | |
707 # Sometimes other windows are accidently opened (such as an extension | |
708 # popup), close them. | |
709 if len(browser.window_handles) > 1: | |
710 close_other_windows(browser, url) | |
711 | |
712 try: | |
713 v = browser.execute_script('return window.finished') | |
714 if v: | |
715 break | |
716 | |
717 try: | |
718 progress = browser.execute_script('return window.getTestRunnerPr
ogress()') | |
719 status = '%s/%s (%s%%)' % (progress['completed'], progress['tota
l'], | |
720 100 * progress['completed'] // progress['total']) | |
721 except selenium_exceptions.WebDriverException, e: | |
722 status = e | |
723 | |
724 print 'Running tests...', status | |
725 sys.stdout.flush() | |
726 time.sleep(1) | |
727 | |
728 # Deal with unexpected alerts, sometimes they are dismissed by | |
729 # alternative means so we have to deal with that case too. | |
730 except selenium_exceptions.UnexpectedAlertPresentException, e: | |
731 try: | |
732 alert = browser.switch_to_alert() | |
733 sys.stderr.write("""\ | |
734 WARNING: Unexpected alert found! | |
735 --------------------------------------------------------------------- | |
736 %s | |
737 --------------------------------------------------------------------- | |
738 """ % alert.text) | |
739 alert.dismiss() | |
740 except selenium_exceptions.NoAlertPresentException, e: | |
741 sys.stderr.write( | |
742 "WARNING: Unexpected alert" | |
743 " which dissappeared on it's own!\n" | |
744 ) | |
745 sys.stderr.flush() | |
746 | |
747 except Exception, e: | |
748 import traceback | |
749 sys.stderr.write(traceback.format_exc()) | |
750 major_failure = True | |
751 | |
752 finally: | |
753 output.stopTestRun() | |
754 | |
755 if args.browser == "Chrome": | |
756 log_path = os.path.join(user_data_dir, "chrome_debug.log") | |
757 if os.path.exists(log_path): | |
758 shutil.copy(log_path, ".") | |
759 else: | |
760 print "Unable to find Chrome log file:", log_path | |
761 | |
762 if summary.testsRun == 0: | |
763 print | |
764 print "FAIL: No tests run!" | |
765 | |
766 sys.stdout.flush() | |
767 sys.stderr.flush() | |
768 | |
769 while args.dontexit and browser.window_handles: | |
770 print "Waiting for you to close the browser...." | |
771 sys.stdout.flush() | |
772 sys.stderr.flush() | |
773 time.sleep(1) | |
774 | |
775 sys.stdout.flush() | |
776 sys.stderr.flush() | |
777 | |
778 # Annotate the success / failure to sauce labs | |
779 if args.sauce and session_id: | |
780 base64string = base64.encodestring( | |
781 '%s:%s' % (sauce_username, sauce_access_key))[:-1] | |
782 | |
783 custom_data["failures"] = summary.failures | |
784 custom_data["errors"] = summary.errors | |
785 custom_data["tests"] = summary.testsRun | |
786 custom_data["skipped"] = summary.skipped | |
787 | |
788 body_content = simplejson.dumps({ | |
789 "passed": summary.wasSuccessful(), | |
790 "custom-data": custom_data, | |
791 }) | |
792 connection = httplib.HTTPConnection("saucelabs.com") | |
793 connection.request( | |
794 'PUT', '/rest/v1/%s/jobs/%s' % (sauce_username, session_id), | |
795 body_content, | |
796 headers={"Authorization": "Basic %s" % base64string}) | |
797 result = connection.getresponse() | |
798 print "Sauce labs updated:", result.status == 200 | |
799 | |
800 import hmac | |
801 from hashlib import md5 | |
802 key = hmac.new( | |
803 str("%s:%s" % (sauce_username, session_id)), | |
804 str(sauce_access_key), | |
805 md5).hexdigest() | |
806 url = "https://saucelabs.com/jobs/%s?auth=%s" % ( | |
807 browser.session_id, key) | |
808 print "Sauce lab output at:", url | |
809 | |
810 if summary.wasSuccessful() and summary.testsRun > 0 and not major_failure: | |
811 sys.exit(0) | |
812 else: | |
813 sys.exit(1) | |
OLD | NEW |