| 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 |