| OLD | NEW |
| 1 from __future__ import print_function | 1 from __future__ import print_function |
| 2 | 2 |
| 3 import argparse | 3 import argparse |
| 4 import json | 4 import json |
| 5 import logging | 5 import logging |
| 6 import os | 6 import os |
| 7 import re | 7 import re |
| 8 import stat | 8 import stat |
| 9 import subprocess | 9 import subprocess |
| 10 import sys | 10 import sys |
| (...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 82 self.name = name | 82 self.name = name |
| 83 | 83 |
| 84 def __enter__(self): | 84 def __enter__(self): |
| 85 print("travis_fold:start:%s" % self.name, file=sys.stderr) | 85 print("travis_fold:start:%s" % self.name, file=sys.stderr) |
| 86 | 86 |
| 87 def __exit__(self, type, value, traceback): | 87 def __exit__(self, type, value, traceback): |
| 88 print("travis_fold:end:%s" % self.name, file=sys.stderr) | 88 print("travis_fold:end:%s" % self.name, file=sys.stderr) |
| 89 | 89 |
| 90 | 90 |
| 91 class GitHub(object): | 91 class GitHub(object): |
| 92 def __init__(self, org, repo, token, browser): | 92 def __init__(self, org, repo, token, product): |
| 93 self.token = token | 93 self.token = token |
| 94 self.headers = {"Accept": "application/vnd.github.v3+json"} | 94 self.headers = {"Accept": "application/vnd.github.v3+json"} |
| 95 self.auth = (self.token, "x-oauth-basic") | 95 self.auth = (self.token, "x-oauth-basic") |
| 96 self.org = org | 96 self.org = org |
| 97 self.repo = repo | 97 self.repo = repo |
| 98 self.base_url = "https://api.github.com/repos/%s/%s/" % (org, repo) | 98 self.base_url = "https://api.github.com/repos/%s/%s/" % (org, repo) |
| 99 self.browser = browser | 99 self.product = product |
| 100 | 100 |
| 101 def _headers(self, headers): | 101 def _headers(self, headers): |
| 102 if headers is None: | 102 if headers is None: |
| 103 headers = {} | 103 headers = {} |
| 104 rv = self.headers.copy() | 104 rv = self.headers.copy() |
| 105 rv.update(headers) | 105 rv.update(headers) |
| 106 return rv | 106 return rv |
| 107 | 107 |
| 108 def post(self, url, data, headers=None): | 108 def post(self, url, data, headers=None): |
| 109 logger.debug("POST %s" % url) | 109 logger.debug("POST %s" % url) |
| (...skipping 28 matching lines...) Expand all Loading... |
| 138 headers=self._headers(headers), | 138 headers=self._headers(headers), |
| 139 auth=self.auth | 139 auth=self.auth |
| 140 ) | 140 ) |
| 141 resp.raise_for_status() | 141 resp.raise_for_status() |
| 142 return resp | 142 return resp |
| 143 | 143 |
| 144 def post_comment(self, issue_number, body): | 144 def post_comment(self, issue_number, body): |
| 145 user = self.get(urljoin(self.base_url, "/user")).json() | 145 user = self.get(urljoin(self.base_url, "/user")).json() |
| 146 issue_comments_url = urljoin(self.base_url, "issues/%s/comments" % issue
_number) | 146 issue_comments_url = urljoin(self.base_url, "issues/%s/comments" % issue
_number) |
| 147 comments = self.get(issue_comments_url).json() | 147 comments = self.get(issue_comments_url).json() |
| 148 title_line = "# %s #" % self.browser.title() | 148 title_line = format_comment_title(self.product) |
| 149 data = {"body": body} | 149 data = {"body": body} |
| 150 for comment in comments: | 150 for comment in comments: |
| 151 if (comment["user"]["login"] == user["login"] and | 151 if (comment["user"]["login"] == user["login"] and |
| 152 comment["body"].startswith(title_line)): | 152 comment["body"].startswith(title_line)): |
| 153 comment_url = urljoin(self.base_url, "issues/comments/%s" % comm
ent["id"]) | 153 comment_url = urljoin(self.base_url, "issues/comments/%s" % comm
ent["id"]) |
| 154 self.patch(comment_url, data) | 154 self.patch(comment_url, data) |
| 155 break | 155 break |
| 156 else: | 156 else: |
| 157 self.post(issue_comments_url, data) | 157 self.post(issue_comments_url, data) |
| 158 | 158 |
| 159 def releases(self): | |
| 160 url = urljoin(self.base_url, "releases/latest") | |
| 161 return self.get(url) | |
| 162 | |
| 163 | 159 |
| 164 class GitHubCommentHandler(logging.Handler): | 160 class GitHubCommentHandler(logging.Handler): |
| 165 def __init__(self, github, pull_number): | 161 def __init__(self, github, pull_number): |
| 166 logging.Handler.__init__(self) | 162 logging.Handler.__init__(self) |
| 167 self.github = github | 163 self.github = github |
| 168 self.pull_number = pull_number | 164 self.pull_number = pull_number |
| 169 self.log_data = [] | 165 self.log_data = [] |
| 170 | 166 |
| 171 def emit(self, record): | 167 def emit(self, record): |
| 172 try: | 168 try: |
| (...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 230 "certutil_binary": "certutil", | 226 "certutil_binary": "certutil", |
| 231 "webdriver_binary": "%s/geckodriver" % root, | 227 "webdriver_binary": "%s/geckodriver" % root, |
| 232 "prefs_root": "%s/profiles" % root, | 228 "prefs_root": "%s/profiles" % root, |
| 233 } | 229 } |
| 234 | 230 |
| 235 | 231 |
| 236 class Chrome(Browser): | 232 class Chrome(Browser): |
| 237 product = "chrome" | 233 product = "chrome" |
| 238 | 234 |
| 239 def install(self): | 235 def install(self): |
| 240 latest = get("https://www.googleapis.com/download/storage/v1/b/chromium-
browser-snapshots/o/Linux_x64%2FLAST_CHANGE?alt=media").text.strip() | 236 # Installing the Google Chrome browser requires administrative |
| 241 url = "https://www.googleapis.com/download/storage/v1/b/chromium-browser
-snapshots/o/Linux_x64%%2F%s%%2Fchrome-linux.zip?alt=media" % latest | 237 # privileges, so that installation is handled by the invoking script. |
| 242 unzip(get(url).raw) | 238 |
| 243 logger.debug(call("ls", "-lhrt", "chrome-linux")) | |
| 244 call("pip", "install", "-r", os.path.join("w3c", "wptrunner", "requireme
nts_chrome.txt")) | 239 call("pip", "install", "-r", os.path.join("w3c", "wptrunner", "requireme
nts_chrome.txt")) |
| 245 | 240 |
| 246 def install_webdriver(self): | 241 def install_webdriver(self): |
| 247 latest = get("http://chromedriver.storage.googleapis.com/LATEST_RELEASE"
).text.strip() | 242 latest = get("http://chromedriver.storage.googleapis.com/LATEST_RELEASE"
).text.strip() |
| 248 url = "http://chromedriver.storage.googleapis.com/%s/chromedriver_linux6
4.zip" % latest | 243 url = "http://chromedriver.storage.googleapis.com/%s/chromedriver_linux6
4.zip" % latest |
| 249 unzip(get(url).raw) | 244 unzip(get(url).raw) |
| 250 st = os.stat('chromedriver') | 245 st = os.stat('chromedriver') |
| 251 os.chmod('chromedriver', st.st_mode | stat.S_IEXEC) | 246 os.chmod('chromedriver', st.st_mode | stat.S_IEXEC) |
| 252 | 247 |
| 253 def wptrunner_args(self, root): | 248 def wptrunner_args(self, root): |
| 254 return { | 249 return { |
| 255 "product": "chrome", | 250 "product": "chrome", |
| 256 "binary": "%s/chrome-linux/chrome" % root, | 251 "binary": "/usr/bin/google-chrome", |
| 252 # Chrome's "sandbox" security feature must be disabled in order to |
| 253 # run the browser in OpenVZ environments such as the one provided |
| 254 # by TravisCI. |
| 255 # |
| 256 # Reference: https://github.com/travis-ci/travis-ci/issues/938 |
| 257 "binary_arg": "--no-sandbox", |
| 257 "webdriver_binary": "%s/chromedriver" % root, | 258 "webdriver_binary": "%s/chromedriver" % root, |
| 258 "test_types": ["testharness", "reftest"] | 259 "test_types": ["testharness", "reftest"] |
| 259 } | 260 } |
| 260 | 261 |
| 261 | 262 |
| 262 def get(url): | 263 def get(url): |
| 263 logger.debug("GET %s" % url) | 264 logger.debug("GET %s" % url) |
| 264 resp = requests.get(url, stream=True) | 265 resp = requests.get(url, stream=True) |
| 265 resp.raise_for_status() | 266 resp.raise_for_status() |
| 266 return resp | 267 return resp |
| (...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 311 with zipfile.ZipFile(fileobj) as zip_data: | 312 with zipfile.ZipFile(fileobj) as zip_data: |
| 312 for info in zip_data.infolist(): | 313 for info in zip_data.infolist(): |
| 313 zip_data.extract(info) | 314 zip_data.extract(info) |
| 314 perm = info.external_attr >> 16 & 0x1FF | 315 perm = info.external_attr >> 16 & 0x1FF |
| 315 os.chmod(info.filename, perm) | 316 os.chmod(info.filename, perm) |
| 316 | 317 |
| 317 | 318 |
| 318 def setup_github_logging(args): | 319 def setup_github_logging(args): |
| 319 gh_handler = None | 320 gh_handler = None |
| 320 if args.comment_pr: | 321 if args.comment_pr: |
| 321 github = GitHub("w3c", "web-platform-tests", args.gh_token, args.browser
) | 322 github = GitHub("w3c", "web-platform-tests", args.gh_token, args.product
) |
| 322 try: | 323 try: |
| 323 pr_number = int(args.comment_pr) | 324 pr_number = int(args.comment_pr) |
| 324 except ValueError: | 325 except ValueError: |
| 325 pass | 326 pass |
| 326 else: | 327 else: |
| 327 gh_handler = GitHubCommentHandler(github, pr_number) | 328 gh_handler = GitHubCommentHandler(github, pr_number) |
| 328 gh_handler.setLevel(logging.INFO) | 329 gh_handler.setLevel(logging.INFO) |
| 329 logger.debug("Setting up GitHub logging") | 330 logger.debug("Setting up GitHub logging") |
| 330 logger.addHandler(gh_handler) | 331 logger.addHandler(gh_handler) |
| 331 else: | 332 else: |
| (...skipping 153 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 485 handler = LogHandler() | 486 handler = LogHandler() |
| 486 reader.handle_log(reader.read(log), handler) | 487 reader.handle_log(reader.read(log), handler) |
| 487 results = handler.results | 488 results = handler.results |
| 488 for test, test_results in results.iteritems(): | 489 for test, test_results in results.iteritems(): |
| 489 for subtest, result in test_results.iteritems(): | 490 for subtest, result in test_results.iteritems(): |
| 490 if is_inconsistent(result, iterations): | 491 if is_inconsistent(result, iterations): |
| 491 inconsistent.append((test, subtest, result)) | 492 inconsistent.append((test, subtest, result)) |
| 492 return results, inconsistent | 493 return results, inconsistent |
| 493 | 494 |
| 494 | 495 |
| 496 def format_comment_title(product): |
| 497 """Produce a Markdown-formatted string based on a given "product"--a string |
| 498 containing a browser identifier optionally followed by a colon and a |
| 499 release channel. (For example: "firefox" or "chrome:dev".) The generated |
| 500 title string is used both to create new comments and to locate (and |
| 501 subsequently update) previously-submitted comments.""" |
| 502 parts = product.split(":") |
| 503 title = parts[0].title() |
| 504 |
| 505 if len(parts) > 1: |
| 506 title += " (%s channel)" % parts[1] |
| 507 |
| 508 return "# %s #" % title |
| 509 |
| 510 |
| 495 def markdown_adjust(s): | 511 def markdown_adjust(s): |
| 496 s = s.replace('\t', u'\\t') | 512 s = s.replace('\t', u'\\t') |
| 497 s = s.replace('\n', u'\\n') | 513 s = s.replace('\n', u'\\n') |
| 498 s = s.replace('\r', u'\\r') | 514 s = s.replace('\r', u'\\r') |
| 499 s = s.replace('`', u'\\`') | 515 s = s.replace('`', u'\\`') |
| 500 return s | 516 return s |
| 501 | 517 |
| 502 | 518 |
| 503 def table(headings, data, log): | 519 def table(headings, data, log): |
| 504 cols = range(len(headings)) | 520 cols = range(len(headings)) |
| (...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 563 type=int, | 579 type=int, |
| 564 help="Number of times to run tests") | 580 help="Number of times to run tests") |
| 565 parser.add_argument("--gh-token", | 581 parser.add_argument("--gh-token", |
| 566 action="store", | 582 action="store", |
| 567 default=os.environ.get("GH_TOKEN"), | 583 default=os.environ.get("GH_TOKEN"), |
| 568 help="OAuth token to use for accessing GitHub api") | 584 help="OAuth token to use for accessing GitHub api") |
| 569 parser.add_argument("--comment-pr", | 585 parser.add_argument("--comment-pr", |
| 570 action="store", | 586 action="store", |
| 571 default=os.environ.get("TRAVIS_PULL_REQUEST"), | 587 default=os.environ.get("TRAVIS_PULL_REQUEST"), |
| 572 help="PR to comment on with stability results") | 588 help="PR to comment on with stability results") |
| 573 parser.add_argument("browser", | 589 parser.add_argument("product", |
| 574 action="store", | 590 action="store", |
| 575 help="Browser to run against") | 591 help="Product to run against (`browser-name` or 'browser
-name:channel')") |
| 576 return parser | 592 return parser |
| 577 | 593 |
| 578 | 594 |
| 579 def main(): | 595 def main(): |
| 580 retcode = 0 | 596 retcode = 0 |
| 581 parser = get_parser() | 597 parser = get_parser() |
| 582 args = parser.parse_args() | 598 args = parser.parse_args() |
| 583 | 599 |
| 584 if not os.path.exists(args.root): | 600 if not os.path.exists(args.root): |
| 585 logger.critical("Root directory %s does not exist" % args.root) | 601 logger.critical("Root directory %s does not exist" % args.root) |
| 586 return 1 | 602 return 1 |
| 587 | 603 |
| 588 os.chdir(args.root) | 604 os.chdir(args.root) |
| 589 | 605 |
| 590 if args.gh_token: | 606 if args.gh_token: |
| 591 gh_handler = setup_github_logging(args) | 607 gh_handler = setup_github_logging(args) |
| 592 else: | 608 else: |
| 593 logger.warning("Can't log to GitHub") | 609 logger.warning("Can't log to GitHub") |
| 594 gh_handler = None | 610 gh_handler = None |
| 595 | 611 |
| 612 browser_name = args.product.split(":")[0] |
| 613 |
| 596 with TravisFold("browser_setup"): | 614 with TravisFold("browser_setup"): |
| 597 logger.info("# %s #" % args.browser.title()) | 615 logger.info(format_comment_title(args.product)) |
| 598 | 616 |
| 599 browser_cls = {"firefox": Firefox, | 617 browser_cls = {"firefox": Firefox, |
| 600 "chrome": Chrome}.get(args.browser) | 618 "chrome": Chrome}.get(browser_name) |
| 601 if browser_cls is None: | 619 if browser_cls is None: |
| 602 logger.critical("Unrecognised browser %s" % args.browser) | 620 logger.critical("Unrecognised browser %s" % browser_name) |
| 603 return 1 | 621 return 1 |
| 604 | 622 |
| 605 fetch_wpt_master() | 623 fetch_wpt_master() |
| 606 | 624 |
| 607 head_sha1 = get_sha1() | 625 head_sha1 = get_sha1() |
| 608 logger.info("Testing revision %s" % head_sha1) | 626 logger.info("Testing revision %s" % head_sha1) |
| 609 | 627 |
| 610 # For now just pass the whole list of changed files to wptrunner and | 628 # For now just pass the whole list of changed files to wptrunner and |
| 611 # assume that it will run everything that's actually a test | 629 # assume that it will run everything that's actually a test |
| 612 files_changed = get_files_changed() | 630 files_changed = get_files_changed() |
| (...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 678 return retcode | 696 return retcode |
| 679 | 697 |
| 680 | 698 |
| 681 if __name__ == "__main__": | 699 if __name__ == "__main__": |
| 682 try: | 700 try: |
| 683 retcode = main() | 701 retcode = main() |
| 684 except: | 702 except: |
| 685 raise | 703 raise |
| 686 else: | 704 else: |
| 687 sys.exit(retcode) | 705 sys.exit(retcode) |
| OLD | NEW |