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

Side by Side Diff: third_party/WebKit/LayoutTests/imported/wpt/check_stability.py

Issue 2446483002: Import wpt@c5a14f553cba5f197743b9af605a84eddd8692a2 (Closed)
Patch Set: Created 4 years, 1 month 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
OLDNEW
(Empty)
1 import argparse
2 import json
3 import logging
4 import os
5 import stat
6 import subprocess
7 import sys
8 import tarfile
9 import traceback
10 import zipfile
11 from cStringIO import StringIO
12 from collections import defaultdict
13 from urlparse import urljoin
14
15 import requests
16
17 wptrunner = None
18 wptcommandline = None
19 reader = None
20 LogHandler = None
21
22 logger = logging.getLogger(os.path.splitext(__file__)[0])
23
24
25 def do_delayed_imports():
26 global wptrunner, wptcommandline, reader
27 from wptrunner import wptrunner
28 from wptrunner import wptcommandline
29 from mozlog import reader
30 setup_log_handler()
31
32
33 def setup_logging():
34 handler = logging.StreamHandler(sys.stdout)
35 formatter = logging.Formatter(logging.BASIC_FORMAT, None)
36 handler.setFormatter(formatter)
37 logger.addHandler(handler)
38 logger.setLevel(logging.DEBUG)
39
40 setup_logging()
41
42
43 class GitHub(object):
44 def __init__(self, org, repo, token):
45 self.token = token
46 self.headers = {"Accept": "application/vnd.github.v3+json"}
47 self.auth = (self.token, "x-oauth-basic")
48 self.org = org
49 self.repo = repo
50 self.base_url = "https://api.github.com/repos/%s/%s/" % (org, repo)
51
52 def _headers(self, headers):
53 if headers is None:
54 headers = {}
55 rv = self.headers.copy()
56 rv.update(headers)
57 return rv
58
59 def post(self, url, data, headers=None):
60 logger.debug("POST %s" % url)
61 if data is not None:
62 data = json.dumps(data)
63 resp = requests.post(
64 url,
65 data=data,
66 headers=self._headers(headers),
67 auth=self.auth
68 )
69 resp.raise_for_status()
70 return resp
71
72 def get(self, url, headers=None):
73 logger.debug("GET %s" % url)
74 resp = requests.get(
75 url,
76 headers=self._headers(headers),
77 auth=self.auth
78 )
79 resp.raise_for_status()
80 return resp
81
82 def post_comment(self, issue_number, body):
83 url = urljoin(self.base_url, "issues/%s/comments" % issue_number)
84 return self.post(url, {"body": body})
85
86 def releases(self):
87 url = urljoin(self.base_url, "releases/latest")
88 return self.get(url)
89
90
91 class GitHubCommentHandler(logging.Handler):
92 def __init__(self, github, pull_number):
93 logging.Handler.__init__(self)
94 self.github = github
95 self.pull_number = pull_number
96 self.log_data = []
97
98 def emit(self, record):
99 try:
100 msg = self.format(record)
101 self.log_data.append(msg)
102 except Exception:
103 self.handleError(record)
104
105 def send(self):
106 self.github.post_comment(self.pull_number, "\n".join(self.log_data))
107 self.log_data = []
108
109
110 class Browser(object):
111 product = None
112
113 def __init__(self, github_token):
114 self.github_token = github_token
115
116
117 class Firefox(Browser):
118 product = "firefox"
119
120 def install(self):
121 call("pip", "install", "-r", "w3c/wptrunner/requirements_firefox.txt")
122 resp = get("https://archive.mozilla.org/pub/firefox/nightly/latest-mozil la-central/firefox-52.0a1.en-US.linux-x86_64.tar.bz2")
123 untar(resp.raw)
124
125 if not os.path.exists("profiles"):
126 os.mkdir("profiles")
127 with open(os.path.join("profiles", "prefs_general.js"), "wb") as f:
128 resp = get("https://hg.mozilla.org/mozilla-central/raw-file/tip/test ing/profiles/prefs_general.js")
129 f.write(resp.content)
130 call("pip", "install", "-r", os.path.join("w3c", "wptrunner", "requireme nts_firefox.txt"))
131
132 def install_webdriver(self):
133 github = GitHub("mozilla", "geckodriver", self.github_token)
134 releases = github.releases().json()
135 url = (item["browser_download_url"] for item in releases["assets"]
136 if "linux64" in item["browser_download_url"]).next()
137 untar(get(url).raw)
138
139 def wptrunner_args(self, root):
140 return {
141 "product": "firefox",
142 "binary": "%s/firefox/firefox" % root,
143 "certutil_binary": "certutil",
144 "webdriver_binary": "%s/geckodriver" % root,
145 "prefs_root": "%s/profiles" % root,
146 }
147
148
149 class Chrome(Browser):
150 product = "chrome"
151
152 def install(self):
153 latest = get("https://www.googleapis.com/download/storage/v1/b/chromium- browser-snapshots/o/Linux_x64%2FLAST_CHANGE?alt=media").text.strip()
154 url = "https://www.googleapis.com/download/storage/v1/b/chromium-browser -snapshots/o/Linux_x64%%2F%s%%2Fchrome-linux.zip?alt=media" % latest
155 unzip(get(url).raw)
156 logger.debug(call("ls", "-lhrt", "chrome-linux"))
157 call("pip", "install", "-r", os.path.join("w3c", "wptrunner", "requireme nts_chrome.txt"))
158
159 def install_webdriver(self):
160 latest = get("http://chromedriver.storage.googleapis.com/LATEST_RELEASE" ).text.strip()
161 url = "http://chromedriver.storage.googleapis.com/%s/chromedriver_linux6 4.zip" % latest
162 unzip(get(url).raw)
163 st = os.stat('chromedriver')
164 os.chmod('chromedriver', st.st_mode | stat.S_IEXEC)
165
166 def wptrunner_args(self, root):
167 return {
168 "product": "chrome",
169 "binary": "%s/chrome-linux/chrome" % root,
170 "webdriver_binary": "%s/chromedriver" % root,
171 "test_types": ["testharness", "reftest"]
172 }
173
174
175 def get(url):
176 logger.debug("GET %s" % url)
177 resp = requests.get(url, stream=True)
178 resp.raise_for_status()
179 return resp
180
181
182 def call(*args):
183 logger.debug("%s" % " ".join(args))
184 return subprocess.check_output(args)
185
186
187 def get_git_cmd(repo_path):
188 def git(cmd, *args):
189 full_cmd = ["git", cmd] + list(args)
190 try:
191 return subprocess.check_output(full_cmd, cwd=repo_path, stderr=subpr ocess.STDOUT)
192 except subprocess.CalledProcessError as e:
193 logger.error("Git command exited with status %i" % e.returncode)
194 logger.error(e.output)
195 sys.exit(1)
196 return git
197
198
199 def seekable(fileobj):
200 try:
201 fileobj.seek(fileobj.tell())
202 except Exception:
203 return StringIO(fileobj.read())
204 else:
205 return fileobj
206
207
208 def untar(fileobj):
209 logger.debug("untar")
210 fileobj = seekable(fileobj)
211 with tarfile.open(fileobj=fileobj) as tar_data:
212 tar_data.extractall()
213
214
215 def unzip(fileobj):
216 logger.debug("unzip")
217 fileobj = seekable(fileobj)
218 with zipfile.ZipFile(fileobj) as zip_data:
219 for info in zip_data.infolist():
220 zip_data.extract(info)
221 perm = info.external_attr >> 16 & 0x1FF
222 os.chmod(info.filename, perm)
223
224
225 def setup_github_logging(args):
226 gh_handler = None
227 if args.comment_pr:
228 github = GitHub("w3c", "web-platform-tests", args.gh_token)
229 try:
230 pr_number = int(args.comment_pr)
231 except ValueError:
232 pass
233 else:
234 gh_handler = GitHubCommentHandler(github, pr_number)
235 gh_handler.setLevel(logging.INFO)
236 logger.debug("Setting up GitHub logging")
237 logger.addHandler(gh_handler)
238 else:
239 logger.warning("No PR number found; not posting to GitHub")
240 return gh_handler
241
242
243 class pwd(object):
244 def __init__(self, dir):
245 self.dir = dir
246 self.old_dir = None
247
248 def __enter__(self):
249 self.old_dir = os.path.abspath(os.curdir)
250 os.chdir(self.dir)
251
252 def __exit__(self, *args, **kwargs):
253 os.chdir(self.old_dir)
254 self.old_dir = None
255
256
257 def fetch_wpt_master():
258 git = get_git_cmd(os.path.join(os.path.abspath(os.curdir), "w3c", "web-platf orm-tests"))
259 git("fetch", "https://github.com/w3c/web-platform-tests.git", "master:master ")
260
261
262 def get_sha1():
263 git = get_git_cmd(os.path.join(os.path.abspath(os.curdir), "w3c", "web-platf orm-tests"))
264 return git("rev-parse", "HEAD").strip()
265
266 def build_manifest():
267 with pwd(os.path.join(os.path.abspath(os.curdir), "w3c", "web-platform-tests ")):
268 # TODO: Call the manifest code directly
269 call("python", "manifest")
270
271
272 def install_wptrunner():
273 call("git", "clone", "--depth=1", "https://github.com/w3c/wptrunner.git", "w 3c/wptrunner")
274 git = get_git_cmd(os.path.join(os.path.abspath(os.curdir), "w3c", "wptrunner "))
275 git("submodule", "update", "--init", "--recursive")
276 call("pip", "install", os.path.join("w3c", "wptrunner"))
277
278
279 def get_files_changed():
280 root = os.path.abspath(os.curdir)
281 git = get_git_cmd("%s/w3c/web-platform-tests" % root)
282 branch_point = git("merge-base", "HEAD", "master").strip()
283 logger.debug("Branch point from master: %s" % branch_point)
284 logger.debug(git("log", "--oneline", "%s.." % branch_point))
285 files = git("diff", "--name-only", "-z", "%s.." % branch_point)
286 if not files:
287 return []
288 assert files[-1] == "\0"
289 return ["%s/w3c/web-platform-tests/%s" % (root, item)
290 for item in files[:-1].split("\0")]
291
292
293 def wptrunner_args(root, files_changed, iterations, browser):
294 parser = wptcommandline.create_parser([browser.product])
295 args = vars(parser.parse_args([]))
296 wpt_root = os.path.join(root, "w3c", "web-platform-tests")
297 args.update(browser.wptrunner_args(root))
298 args.update({
299 "tests_root": wpt_root,
300 "metadata_root": wpt_root,
301 "repeat": iterations,
302 "config": "%s/w3c/wptrunner/wptrunner.default.ini" % root,
303 "test_list": files_changed,
304 "restart_on_unexpected": False,
305 "pause_after_test": False
306 })
307 wptcommandline.check_args(args)
308 return args
309
310
311 def setup_log_handler():
312 global LogHandler
313
314 class LogHandler(reader.LogHandler):
315 def __init__(self):
316 self.results = defaultdict(lambda: defaultdict(lambda: defaultdict(i nt)))
317
318 def test_status(self, data):
319 self.results[data["test"]][data.get("subtest")][data["status"]] += 1
320
321 def test_end(self, data):
322 self.results[data["test"]][None][data["status"]] += 1
323
324
325 def is_inconsistent(results_dict, iterations):
326 return len(results_dict) > 1 or sum(results_dict.values()) != iterations
327
328
329 def err_string(results_dict, iterations):
330 rv = []
331 total_results = sum(results_dict.values())
332 for key, value in sorted(results_dict.items()):
333 rv.append("%s%s" %
334 (key, ": %s/%s" % (value, iterations) if value != iterations e lse ""))
335 rv = ", ".join(rv)
336 if total_results < iterations:
337 rv.append("MISSING: %s/%s" % (iterations - total_results, iterations))
338 if len(results_dict) > 1 or total_results != iterations:
339 rv = "**%s**" % rv
340 return rv
341
342
343 def process_results(log, iterations):
344 inconsistent = []
345 handler = LogHandler()
346 reader.handle_log(reader.read(log), handler)
347 results = handler.results
348 for test, test_results in results.iteritems():
349 for subtest, result in test_results.iteritems():
350 if is_inconsistent(result, iterations):
351 inconsistent.append((test, subtest, result))
352 return results, inconsistent
353
354
355 def write_inconsistent(inconsistent, iterations):
356 logger.error("## Unstable results ##\n")
357 logger.error("| Test | Subtest | Results |")
358 logger.error("|------|---------|---------|")
359 for test, subtest, results in inconsistent:
360 logger.error("%s | %s | %s" % (test,
361 subtest if subtest else "",
362 err_string(results, iterations)))
363
364
365 def write_results(results, iterations):
366 logger.info("## All results ##\n")
367 for test, test_results in results.iteritems():
368 logger.info("### %s ###" % test)
369 logger.info("| Subtest | Results |")
370 logger.info("|---------|---------|")
371 parent = test_results.pop(None)
372 logger.info("| | %s |" % (err_string(parent, iterations)))
373 for subtest, result in test_results.iteritems():
374 logger.info("| %s | %s |" % (subtest, err_string(result, iterations) ))
375
376
377 def get_parser():
378 parser = argparse.ArgumentParser()
379 parser.add_argument("--root",
380 action="store",
381 default=os.path.join(os.path.expanduser("~"), "build"),
382 help="Root path")
383 parser.add_argument("--iterations",
384 action="store",
385 default=10,
386 type=int,
387 help="Number of times to run tests")
388 parser.add_argument("--gh-token",
389 action="store",
390 default=os.environ.get("GH_TOKEN"),
391 help="OAuth token to use for accessing GitHub api")
392 parser.add_argument("--comment-pr",
393 action="store",
394 default=os.environ.get("TRAVIS_PULL_REQUEST"),
395 help="PR to comment on with stability results")
396 parser.add_argument("browser",
397 action="store",
398 help="Browser to run against")
399 return parser
400
401
402 def main():
403 retcode = 0
404 parser = get_parser()
405 args = parser.parse_args()
406
407 if not os.path.exists(args.root):
408 logger.critical("Root directory %s does not exist" % args.root)
409 return 1
410
411 os.chdir(args.root)
412
413 if args.gh_token is None:
414 logger.critical("Must provide a GitHub token via --gh-token or $GITHUB_T OKEN")
415 return 1
416
417 gh_handler = setup_github_logging(args)
418
419 logger.info("# %s #" % args.browser.title())
420
421 browser_cls = {"firefox": Firefox,
422 "chrome": Chrome}.get(args.browser)
423 if browser_cls is None:
424 logger.critical("Unrecognised browser %s" % args.browser)
425 return 1
426
427 fetch_wpt_master()
428
429 head_sha1 = get_sha1()
430 logger.info("Testing revision %s" % head_sha1)
431
432 # For now just pass the whole list of changed files to wptrunner and
433 # assume that it will run everything that's actually a test
434 files_changed = get_files_changed()
435
436 if not files_changed:
437 logger.info("No files changed")
438 return 0
439
440 build_manifest()
441 install_wptrunner()
442 do_delayed_imports()
443
444 logger.debug("Files changed:\n%s" % "".join(" * %s\n" % item for item in fil es_changed))
445
446 browser = browser_cls(args.gh_token)
447
448 browser.install()
449 browser.install_webdriver()
450
451 kwargs = wptrunner_args(args.root,
452 files_changed,
453 args.iterations,
454 browser)
455 with open("raw.log", "wb") as log:
456 wptrunner.setup_logging(kwargs,
457 {"tbpl": sys.stdout,
458 "raw": log})
459 wptrunner.run_tests(**kwargs)
460
461 with open("raw.log", "rb") as log:
462 results, inconsistent = process_results(log, args.iterations)
463
464 if results:
465 if inconsistent:
466 write_inconsistent(inconsistent, args.iterations)
467 retcode = 2
468 else:
469 logger.info("All results were stable\n")
470 write_results(results, args.iterations)
471 else:
472 logger.info("No tests run.")
473
474 try:
475 if gh_handler:
476 gh_handler.send()
477 except Exception:
478 logger.error(traceback.format_exc())
479 return retcode
480
481
482 if __name__ == "__main__":
483 try:
484 retcode = main()
485 except:
486 raise
487 else:
488 sys.exit(retcode)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698