OLD | NEW |
(Empty) | |
| 1 import argparse |
| 2 import json |
| 3 import logging |
| 4 import os |
| 5 import subprocess |
| 6 import sys |
| 7 import traceback |
| 8 from collections import defaultdict |
| 9 |
| 10 import requests |
| 11 |
| 12 from wptrunner import wptrunner |
| 13 from wptrunner import wptcommandline |
| 14 from mozlog import reader |
| 15 |
| 16 logger = logging.getLogger(os.path.splitext(__file__)[0]) |
| 17 |
| 18 |
| 19 def setup_logging(): |
| 20 handler = logging.StreamHandler(sys.stdout) |
| 21 formatter = logging.Formatter(logging.BASIC_FORMAT, None) |
| 22 handler.setFormatter(formatter) |
| 23 logger.addHandler(handler) |
| 24 logger.setLevel(logging.DEBUG) |
| 25 |
| 26 setup_logging() |
| 27 |
| 28 |
| 29 def setup_github_logging(args): |
| 30 gh_handler = None |
| 31 if args.comment_pr: |
| 32 if args.gh_token: |
| 33 try: |
| 34 pr_number = int(args.comment_pr) |
| 35 except ValueError: |
| 36 pass |
| 37 else: |
| 38 gh_handler = GitHubCommentHandler(args.gh_token, pr_number) |
| 39 logger.debug("Setting up GitHub logging") |
| 40 logger.addHandler(gh_handler) |
| 41 else: |
| 42 logger.error("Must provide --comment-pr and --github-token together"
) |
| 43 return gh_handler |
| 44 |
| 45 |
| 46 class GitHubCommentHandler(logging.Handler): |
| 47 def __init__(self, token, pull_number): |
| 48 logging.Handler.__init__(self) |
| 49 self.token = token |
| 50 self.pull_number = pull_number |
| 51 self.log_data = [] |
| 52 |
| 53 def emit(self, record): |
| 54 try: |
| 55 msg = self.format(record) |
| 56 self.log_data.append(msg) |
| 57 except Exception: |
| 58 self.handleError(record) |
| 59 |
| 60 def send(self): |
| 61 headers = {"Accept": "application/vnd.github.v3+json"} |
| 62 auth = (self.token, "x-oauth-basic") |
| 63 url = "https://api.github.com/repos/w3c/web-platform-tests/issues/%s/com
ments" %( |
| 64 self.pull_number,) |
| 65 resp = requests.post( |
| 66 url, |
| 67 data=json.dumps({"body": "\n".join(self.log_data)}), |
| 68 headers=headers, |
| 69 auth=auth |
| 70 ) |
| 71 resp.raise_for_status() |
| 72 self.log_data = [] |
| 73 |
| 74 |
| 75 class Firefox(object): |
| 76 product = "firefox" |
| 77 |
| 78 def wptrunner_args(self, root): |
| 79 return { |
| 80 "product": "firefox", |
| 81 "binary": "%s/firefox/firefox" % root, |
| 82 "certutil_binary": "certutil", |
| 83 "webdriver_binary": "%s/geckodriver" % root, |
| 84 "prefs_root": "%s/profiles" % root, |
| 85 } |
| 86 |
| 87 |
| 88 class Chrome(object): |
| 89 product = "chrome" |
| 90 |
| 91 def wptrunner_args(self, root): |
| 92 return { |
| 93 "product": "chrome", |
| 94 "binary": "%s/chrome-linux/chrome" % root, |
| 95 "webdriver_binary": "%s/chromedriver" % root, |
| 96 "test_types": ["testharness", "reftest"] |
| 97 } |
| 98 |
| 99 |
| 100 def get_git_cmd(repo_path): |
| 101 def git(cmd, *args): |
| 102 full_cmd = ["git", cmd] + list(args) |
| 103 try: |
| 104 return subprocess.check_output(full_cmd, cwd=repo_path, stderr=subpr
ocess.STDOUT) |
| 105 except subprocess.CalledProcessError as e: |
| 106 logger.error("Git command exited with status %i" % e.returncode) |
| 107 logger.error(e.output) |
| 108 sys.exit(1) |
| 109 return git |
| 110 |
| 111 |
| 112 def get_files_changed(root): |
| 113 git = get_git_cmd("%s/w3c/web-platform-tests" % root) |
| 114 branch_point = git("merge-base", "HEAD", "master").strip() |
| 115 files = git("diff", "--name-only", "-z", "%s.." % branch_point) |
| 116 if not files: |
| 117 return [] |
| 118 assert files[-1] == "\0" |
| 119 return ["%s/w3c/web-platform-tests/%s" % (root, item) |
| 120 for item in files[:-1].split("\0")] |
| 121 |
| 122 |
| 123 def wptrunner_args(root, files_changed, iterations, browser): |
| 124 parser = wptcommandline.create_parser([browser.product]) |
| 125 args = vars(parser.parse_args([])) |
| 126 wpt_root = os.path.join(root, "w3c", "web-platform-tests") |
| 127 args.update(browser.wptrunner_args(root)) |
| 128 args.update({ |
| 129 "tests_root": wpt_root, |
| 130 "metadata_root": wpt_root, |
| 131 "repeat": iterations, |
| 132 "config": "%s/w3c/wptrunner/wptrunner.default.ini" % root, |
| 133 "test_list": files_changed, |
| 134 "restart_on_unexpected": False, |
| 135 "pause_after_test": False |
| 136 }) |
| 137 wptcommandline.check_args(args) |
| 138 return args |
| 139 |
| 140 |
| 141 class LogHandler(reader.LogHandler): |
| 142 def __init__(self): |
| 143 self.results = defaultdict(lambda: defaultdict(lambda: defaultdict(int))
) |
| 144 |
| 145 def test_status(self, data): |
| 146 self.results[data["test"]][data.get("subtest")][data["status"]] += 1 |
| 147 |
| 148 def test_end(self, data): |
| 149 self.results[data["test"]][None][data["status"]] += 1 |
| 150 |
| 151 |
| 152 def is_inconsistent(results_dict, iterations): |
| 153 return len(results_dict) > 1 or sum(results_dict.values()) != iterations |
| 154 |
| 155 |
| 156 def err_string(results_dict): |
| 157 rv = [] |
| 158 for key, value in sorted(results_dict.items()): |
| 159 rv.append("%s: %i" % (key, value)) |
| 160 rv = " ".join(rv) |
| 161 if len(results_dict) > 1: |
| 162 rv = "**%s**" % rv |
| 163 return rv |
| 164 |
| 165 |
| 166 def process_results(log, iterations): |
| 167 inconsistent = [] |
| 168 handler = LogHandler() |
| 169 reader.handle_log(reader.read(log), handler) |
| 170 results = handler.results |
| 171 for test, test_results in results.iteritems(): |
| 172 for subtest, result in test_results.iteritems(): |
| 173 if is_inconsistent(result, iterations): |
| 174 inconsistent.append((test, subtest, result)) |
| 175 return results, inconsistent |
| 176 |
| 177 |
| 178 def write_inconsistent(inconsistent): |
| 179 logger.error("## Unstable results ##\n") |
| 180 logger.error("| Test | Subtest | Results |") |
| 181 logger.error("|------|---------|---------|") |
| 182 for test, subtest, results in inconsistent: |
| 183 logger.error("%s | %s | %s" % (test, |
| 184 subtest if subtest else "(parent)", |
| 185 err_string(results))) |
| 186 |
| 187 |
| 188 def write_results(results, iterations): |
| 189 logger.info("## All results ##\n") |
| 190 logger.info("| Test | Subtest | Results |") |
| 191 logger.info("|------|---------|---------|") |
| 192 for test, test_results in results.iteritems(): |
| 193 parent = test_results.pop(None) |
| 194 logger.info("| %s | | %s |" % (test, err_string(parent))) |
| 195 for subtest, result in test_results.iteritems(): |
| 196 logger.info("| | %s | %s |" % (subtest, err_string(result))) |
| 197 |
| 198 |
| 199 def get_parser(): |
| 200 parser = argparse.ArgumentParser() |
| 201 parser.add_argument("--root", |
| 202 action="store", |
| 203 default=os.path.join(os.path.expanduser("~"), "build"), |
| 204 help="Root path") |
| 205 parser.add_argument("--iterations", |
| 206 action="store", |
| 207 default=10, |
| 208 type=int, |
| 209 help="Number of times to run tests") |
| 210 parser.add_argument("--gh-token", |
| 211 action="store", |
| 212 help="OAuth token to use for accessing GitHub api") |
| 213 parser.add_argument("--comment-pr", |
| 214 action="store", |
| 215 help="PR to comment on with stability results") |
| 216 parser.add_argument("browser", |
| 217 action="store", |
| 218 help="Browser to run against") |
| 219 return parser |
| 220 |
| 221 |
| 222 def main(): |
| 223 retcode = 0 |
| 224 parser = get_parser() |
| 225 args = parser.parse_args() |
| 226 |
| 227 gh_handler = setup_github_logging(args) |
| 228 |
| 229 logger.info("Testing in **%s**" % args.browser.title()) |
| 230 |
| 231 browser_cls = {"firefox": Firefox, |
| 232 "chrome": Chrome}.get(args.browser) |
| 233 if browser_cls is None: |
| 234 logger.critical("Unrecognised browser %s" % args.browser) |
| 235 return 2 |
| 236 |
| 237 # For now just pass the whole list of changed files to wptrunner and |
| 238 # assume that it will run everything that's actually a test |
| 239 files_changed = get_files_changed(args.root) |
| 240 |
| 241 if not files_changed: |
| 242 return 0 |
| 243 |
| 244 logger.info("Files changed:\n%s" % "".join(" * %s\n" % item for item in file
s_changed)) |
| 245 |
| 246 browser = browser_cls() |
| 247 kwargs = wptrunner_args(args.root, |
| 248 files_changed, |
| 249 args.iterations, |
| 250 browser) |
| 251 with open("raw.log", "wb") as log: |
| 252 wptrunner.setup_logging(kwargs, |
| 253 {"mach": sys.stdout, |
| 254 "raw": log}) |
| 255 wptrunner.run_tests(**kwargs) |
| 256 |
| 257 with open("raw.log", "rb") as log: |
| 258 results, inconsistent = process_results(log, args.iterations) |
| 259 |
| 260 if results: |
| 261 if inconsistent: |
| 262 write_inconsistent(inconsistent) |
| 263 retcode = 1 |
| 264 else: |
| 265 logger.info("All results were stable\n") |
| 266 write_results(results, args.iterations) |
| 267 else: |
| 268 logger.info("No tests run.") |
| 269 |
| 270 try: |
| 271 if gh_handler: |
| 272 gh_handler.send() |
| 273 except Exception: |
| 274 logger.error(traceback.format_exc()) |
| 275 return retcode |
| 276 |
| 277 |
| 278 if __name__ == "__main__": |
| 279 try: |
| 280 retcode = main() |
| 281 except: |
| 282 raise |
| 283 else: |
| 284 sys.exit(retcode) |
OLD | NEW |