Index: third_party/WebKit/LayoutTests/imported/wpt/check_stability.py |
diff --git a/third_party/WebKit/LayoutTests/imported/wpt/check_stability.py b/third_party/WebKit/LayoutTests/imported/wpt/check_stability.py |
index aa504189057ee0f7d370f6e261258ae085497bd3..25496801c5e7a082791a260aa1097cb8696d8313 100644 |
--- a/third_party/WebKit/LayoutTests/imported/wpt/check_stability.py |
+++ b/third_party/WebKit/LayoutTests/imported/wpt/check_stability.py |
@@ -2,20 +2,35 @@ import argparse |
import json |
import logging |
import os |
+import re |
+import stat |
import subprocess |
import sys |
+import tarfile |
import traceback |
+import zipfile |
+from cStringIO import StringIO |
from collections import defaultdict |
+from urlparse import urljoin |
import requests |
-from wptrunner import wptrunner |
-from wptrunner import wptcommandline |
-from mozlog import reader |
+wptrunner = None |
+wptcommandline = None |
+reader = None |
+LogHandler = None |
logger = logging.getLogger(os.path.splitext(__file__)[0]) |
+def do_delayed_imports(): |
+ global wptrunner, wptcommandline, reader |
+ from wptrunner import wptrunner |
+ from wptrunner import wptcommandline |
+ from mozlog import reader |
+ setup_log_handler() |
+ |
+ |
def setup_logging(): |
handler = logging.StreamHandler(sys.stdout) |
formatter = logging.Formatter(logging.BASIC_FORMAT, None) |
@@ -26,27 +41,58 @@ def setup_logging(): |
setup_logging() |
-def setup_github_logging(args): |
- gh_handler = None |
- if args.comment_pr: |
- if args.gh_token: |
- try: |
- pr_number = int(args.comment_pr) |
- except ValueError: |
- pass |
- else: |
- gh_handler = GitHubCommentHandler(args.gh_token, pr_number) |
- logger.debug("Setting up GitHub logging") |
- logger.addHandler(gh_handler) |
- else: |
- logger.error("Must provide --comment-pr and --github-token together") |
- return gh_handler |
+class GitHub(object): |
+ def __init__(self, org, repo, token): |
+ self.token = token |
+ self.headers = {"Accept": "application/vnd.github.v3+json"} |
+ self.auth = (self.token, "x-oauth-basic") |
+ self.org = org |
+ self.repo = repo |
+ self.base_url = "https://api.github.com/repos/%s/%s/" % (org, repo) |
+ |
+ def _headers(self, headers): |
+ if headers is None: |
+ headers = {} |
+ rv = self.headers.copy() |
+ rv.update(headers) |
+ return rv |
+ |
+ def post(self, url, data, headers=None): |
+ logger.debug("POST %s" % url) |
+ if data is not None: |
+ data = json.dumps(data) |
+ resp = requests.post( |
+ url, |
+ data=data, |
+ headers=self._headers(headers), |
+ auth=self.auth |
+ ) |
+ resp.raise_for_status() |
+ return resp |
+ |
+ def get(self, url, headers=None): |
+ logger.debug("GET %s" % url) |
+ resp = requests.get( |
+ url, |
+ headers=self._headers(headers), |
+ auth=self.auth |
+ ) |
+ resp.raise_for_status() |
+ return resp |
+ |
+ def post_comment(self, issue_number, body): |
+ url = urljoin(self.base_url, "issues/%s/comments" % issue_number) |
+ return self.post(url, {"body": body}) |
+ |
+ def releases(self): |
+ url = urljoin(self.base_url, "releases/latest") |
+ return self.get(url) |
class GitHubCommentHandler(logging.Handler): |
- def __init__(self, token, pull_number): |
+ def __init__(self, github, pull_number): |
logging.Handler.__init__(self) |
- self.token = token |
+ self.github = github |
self.pull_number = pull_number |
self.log_data = [] |
@@ -58,23 +104,55 @@ class GitHubCommentHandler(logging.Handler): |
self.handleError(record) |
def send(self): |
- headers = {"Accept": "application/vnd.github.v3+json"} |
- auth = (self.token, "x-oauth-basic") |
- url = "https://api.github.com/repos/w3c/web-platform-tests/issues/%s/comments" %( |
- self.pull_number,) |
- resp = requests.post( |
- url, |
- data=json.dumps({"body": "\n".join(self.log_data)}), |
- headers=headers, |
- auth=auth |
- ) |
- resp.raise_for_status() |
+ self.github.post_comment(self.pull_number, "\n".join(self.log_data)) |
self.log_data = [] |
-class Firefox(object): |
+class Browser(object): |
+ product = None |
+ |
+ def __init__(self, github_token): |
+ self.github_token = github_token |
+ |
+ |
+class Firefox(Browser): |
product = "firefox" |
+ def install(self): |
+ call("pip", "install", "-r", "w3c/wptrunner/requirements_firefox.txt") |
+ resp = get("https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-52.0a1.en-US.linux-x86_64.tar.bz2") |
+ untar(resp.raw) |
+ |
+ if not os.path.exists("profiles"): |
+ os.mkdir("profiles") |
+ with open(os.path.join("profiles", "prefs_general.js"), "wb") as f: |
+ resp = get("https://hg.mozilla.org/mozilla-central/raw-file/tip/testing/profiles/prefs_general.js") |
+ f.write(resp.content) |
+ call("pip", "install", "-r", os.path.join("w3c", "wptrunner", "requirements_firefox.txt")) |
+ |
+ def _latest_geckodriver_version(self): |
+ # This is used rather than an API call to avoid rate limits |
+ tags = call("git", "ls-remote", "--tags", "--refs", |
+ "https://github.com/mozilla/geckodriver.git") |
+ logger.debug("Found tags:\n%s" % tags) |
+ release_re = re.compile(".*refs/tags/v(\d+)\.(\d+)\.(\d+)") |
+ latest_release = 0 |
+ for item in tags.split("\n"): |
+ m = release_re.match(item) |
+ if m: |
+ version = [int(item) for item in m.groups()] |
+ if version > latest_release: |
+ latest_release = version |
+ assert latest_release != 0 |
+ return "v%s.%s.%s" % tuple(str(item) for item in latest_release) |
+ |
+ |
+ def install_webdriver(self): |
+ version = self._latest_geckodriver_version() |
+ logger.debug("Latest geckodriver release %s" % version) |
+ url = "https://github.com/mozilla/geckodriver/releases/download/%s/geckodriver-%s-linux64.tar.gz" % (version, version) |
+ untar(get(url).raw) |
+ |
def wptrunner_args(self, root): |
return { |
"product": "firefox", |
@@ -85,9 +163,23 @@ class Firefox(object): |
} |
-class Chrome(object): |
+class Chrome(Browser): |
product = "chrome" |
+ def install(self): |
+ latest = get("https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2FLAST_CHANGE?alt=media").text.strip() |
+ url = "https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%%2F%s%%2Fchrome-linux.zip?alt=media" % latest |
+ unzip(get(url).raw) |
+ logger.debug(call("ls", "-lhrt", "chrome-linux")) |
+ call("pip", "install", "-r", os.path.join("w3c", "wptrunner", "requirements_chrome.txt")) |
+ |
+ def install_webdriver(self): |
+ latest = get("http://chromedriver.storage.googleapis.com/LATEST_RELEASE").text.strip() |
+ url = "http://chromedriver.storage.googleapis.com/%s/chromedriver_linux64.zip" % latest |
+ unzip(get(url).raw) |
+ st = os.stat('chromedriver') |
+ os.chmod('chromedriver', st.st_mode | stat.S_IEXEC) |
+ |
def wptrunner_args(self, root): |
return { |
"product": "chrome", |
@@ -97,6 +189,18 @@ class Chrome(object): |
} |
+def get(url): |
+ logger.debug("GET %s" % url) |
+ resp = requests.get(url, stream=True) |
+ resp.raise_for_status() |
+ return resp |
+ |
+ |
+def call(*args): |
+ logger.debug("%s" % " ".join(args)) |
+ return subprocess.check_output(args) |
+ |
+ |
def get_git_cmd(repo_path): |
def git(cmd, *args): |
full_cmd = ["git", cmd] + list(args) |
@@ -109,9 +213,92 @@ def get_git_cmd(repo_path): |
return git |
-def get_files_changed(root): |
+def seekable(fileobj): |
+ try: |
+ fileobj.seek(fileobj.tell()) |
+ except Exception: |
+ return StringIO(fileobj.read()) |
+ else: |
+ return fileobj |
+ |
+ |
+def untar(fileobj): |
+ logger.debug("untar") |
+ fileobj = seekable(fileobj) |
+ with tarfile.open(fileobj=fileobj) as tar_data: |
+ tar_data.extractall() |
+ |
+ |
+def unzip(fileobj): |
+ logger.debug("unzip") |
+ fileobj = seekable(fileobj) |
+ with zipfile.ZipFile(fileobj) as zip_data: |
+ for info in zip_data.infolist(): |
+ zip_data.extract(info) |
+ perm = info.external_attr >> 16 & 0x1FF |
+ os.chmod(info.filename, perm) |
+ |
+ |
+def setup_github_logging(args): |
+ gh_handler = None |
+ if args.comment_pr: |
+ github = GitHub("w3c", "web-platform-tests", args.gh_token) |
+ try: |
+ pr_number = int(args.comment_pr) |
+ except ValueError: |
+ pass |
+ else: |
+ gh_handler = GitHubCommentHandler(github, pr_number) |
+ gh_handler.setLevel(logging.INFO) |
+ logger.debug("Setting up GitHub logging") |
+ logger.addHandler(gh_handler) |
+ else: |
+ logger.warning("No PR number found; not posting to GitHub") |
+ return gh_handler |
+ |
+ |
+class pwd(object): |
+ def __init__(self, dir): |
+ self.dir = dir |
+ self.old_dir = None |
+ |
+ def __enter__(self): |
+ self.old_dir = os.path.abspath(os.curdir) |
+ os.chdir(self.dir) |
+ |
+ def __exit__(self, *args, **kwargs): |
+ os.chdir(self.old_dir) |
+ self.old_dir = None |
+ |
+ |
+def fetch_wpt_master(): |
+ git = get_git_cmd(os.path.join(os.path.abspath(os.curdir), "w3c", "web-platform-tests")) |
+ git("fetch", "https://github.com/w3c/web-platform-tests.git", "master:master") |
+ |
+ |
+def get_sha1(): |
+ git = get_git_cmd(os.path.join(os.path.abspath(os.curdir), "w3c", "web-platform-tests")) |
+ return git("rev-parse", "HEAD").strip() |
+ |
+def build_manifest(): |
+ with pwd(os.path.join(os.path.abspath(os.curdir), "w3c", "web-platform-tests")): |
+ # TODO: Call the manifest code directly |
+ call("python", "manifest") |
+ |
+ |
+def install_wptrunner(): |
+ call("git", "clone", "--depth=1", "https://github.com/w3c/wptrunner.git", "w3c/wptrunner") |
+ git = get_git_cmd(os.path.join(os.path.abspath(os.curdir), "w3c", "wptrunner")) |
+ git("submodule", "update", "--init", "--recursive") |
+ call("pip", "install", os.path.join("w3c", "wptrunner")) |
+ |
+ |
+def get_files_changed(): |
+ root = os.path.abspath(os.curdir) |
git = get_git_cmd("%s/w3c/web-platform-tests" % root) |
branch_point = git("merge-base", "HEAD", "master").strip() |
+ logger.debug("Branch point from master: %s" % branch_point) |
+ logger.debug(git("log", "--oneline", "%s.." % branch_point)) |
files = git("diff", "--name-only", "-z", "%s.." % branch_point) |
if not files: |
return [] |
@@ -138,27 +325,34 @@ def wptrunner_args(root, files_changed, iterations, browser): |
return args |
-class LogHandler(reader.LogHandler): |
- def __init__(self): |
- self.results = defaultdict(lambda: defaultdict(lambda: defaultdict(int))) |
+def setup_log_handler(): |
+ global LogHandler |
- def test_status(self, data): |
- self.results[data["test"]][data.get("subtest")][data["status"]] += 1 |
+ class LogHandler(reader.LogHandler): |
+ def __init__(self): |
+ self.results = defaultdict(lambda: defaultdict(lambda: defaultdict(int))) |
- def test_end(self, data): |
- self.results[data["test"]][None][data["status"]] += 1 |
+ def test_status(self, data): |
+ self.results[data["test"]][data.get("subtest")][data["status"]] += 1 |
+ |
+ def test_end(self, data): |
+ self.results[data["test"]][None][data["status"]] += 1 |
def is_inconsistent(results_dict, iterations): |
return len(results_dict) > 1 or sum(results_dict.values()) != iterations |
-def err_string(results_dict): |
+def err_string(results_dict, iterations): |
rv = [] |
+ total_results = sum(results_dict.values()) |
for key, value in sorted(results_dict.items()): |
- rv.append("%s: %i" % (key, value)) |
- rv = " ".join(rv) |
- if len(results_dict) > 1: |
+ rv.append("%s%s" % |
+ (key, ": %s/%s" % (value, iterations) if value != iterations else "")) |
+ rv = ", ".join(rv) |
+ if total_results < iterations: |
+ rv.append("MISSING: %s/%s" % (iterations - total_results, iterations)) |
+ if len(results_dict) > 1 or total_results != iterations: |
rv = "**%s**" % rv |
return rv |
@@ -175,25 +369,37 @@ def process_results(log, iterations): |
return results, inconsistent |
-def write_inconsistent(inconsistent): |
+def table(headings, data, log): |
+ cols = range(len(headings)) |
+ assert all(len(item) == len(cols) for item in data) |
+ max_widths = reduce(lambda prev, cur: [(len(cur[i]) + 2) |
+ if (len(cur[i]) + 2) > prev[i] |
+ else prev[i] |
+ for i in cols], |
+ data, |
+ [len(item) + 2 for item in headings]) |
+ log("|%s|" % "|".join(item.center(max_widths[i]) for i, item in enumerate(headings))) |
+ log("|%s|" % "|".join("-" * max_widths[i] for i in cols)) |
+ for row in data: |
+ log("|%s|" % "|".join(" %s" % row[i].ljust(max_widths[i] - 1) for i in cols)) |
+ |
+ |
+def write_inconsistent(inconsistent, iterations): |
logger.error("## Unstable results ##\n") |
- logger.error("| Test | Subtest | Results |") |
- logger.error("|------|---------|---------|") |
- for test, subtest, results in inconsistent: |
- logger.error("%s | %s | %s" % (test, |
- subtest if subtest else "(parent)", |
- err_string(results))) |
+ strings = [(test, subtest if subtest else "", err_string(results, iterations)) |
+ for test, subtest, results in inconsistent] |
+ table(["Test", "Subtest", "Results"], strings, logger.error) |
def write_results(results, iterations): |
logger.info("## All results ##\n") |
- logger.info("| Test | Subtest | Results |") |
- logger.info("|------|---------|---------|") |
for test, test_results in results.iteritems(): |
+ logger.info("### %s ###" % test) |
parent = test_results.pop(None) |
- logger.info("| %s | | %s |" % (test, err_string(parent))) |
- for subtest, result in test_results.iteritems(): |
- logger.info("| | %s | %s |" % (subtest, err_string(result))) |
+ strings = [("", err_string(parent, iterations))] |
+ strings.extend(((subtest if subtest else "", err_string(results, iterations)) |
+ for subtest, results in test_results.iteritems())) |
+ table(["Subtest", "Results"], strings, logger.info) |
def get_parser(): |
@@ -209,9 +415,11 @@ def get_parser(): |
help="Number of times to run tests") |
parser.add_argument("--gh-token", |
action="store", |
+ default=os.environ.get("GH_TOKEN"), |
help="OAuth token to use for accessing GitHub api") |
parser.add_argument("--comment-pr", |
action="store", |
+ default=os.environ.get("TRAVIS_PULL_REQUEST"), |
help="PR to comment on with stability results") |
parser.add_argument("browser", |
action="store", |
@@ -224,46 +432,78 @@ def main(): |
parser = get_parser() |
args = parser.parse_args() |
- gh_handler = setup_github_logging(args) |
+ if not os.path.exists(args.root): |
+ logger.critical("Root directory %s does not exist" % args.root) |
+ return 1 |
- logger.info("Testing in **%s**" % args.browser.title()) |
+ os.chdir(args.root) |
+ |
+ if args.gh_token: |
+ gh_handler = setup_github_logging(args) |
+ else: |
+ logger.warning("Can't log to GitHub") |
+ gh_handler = None |
+ |
+ print >> sys.stderr, "travis_fold:start:browser_setup" |
+ logger.info("# %s #" % args.browser.title()) |
browser_cls = {"firefox": Firefox, |
"chrome": Chrome}.get(args.browser) |
if browser_cls is None: |
logger.critical("Unrecognised browser %s" % args.browser) |
- return 2 |
+ return 1 |
+ |
+ fetch_wpt_master() |
+ |
+ head_sha1 = get_sha1() |
+ logger.info("Testing revision %s" % head_sha1) |
# For now just pass the whole list of changed files to wptrunner and |
# assume that it will run everything that's actually a test |
- files_changed = get_files_changed(args.root) |
+ files_changed = get_files_changed() |
if not files_changed: |
+ logger.info("No files changed") |
return 0 |
- logger.info("Files changed:\n%s" % "".join(" * %s\n" % item for item in files_changed)) |
+ build_manifest() |
+ install_wptrunner() |
+ do_delayed_imports() |
+ |
+ logger.debug("Files changed:\n%s" % "".join(" * %s\n" % item for item in files_changed)) |
+ |
+ browser = browser_cls(args.gh_token) |
+ |
+ browser.install() |
+ browser.install_webdriver() |
- browser = browser_cls() |
kwargs = wptrunner_args(args.root, |
files_changed, |
args.iterations, |
browser) |
+ |
+ print >> sys.stderr, "travis_fold:end:browser_setup" |
+ print >> sys.stderr, "travis_fold:start:running_tests" |
with open("raw.log", "wb") as log: |
wptrunner.setup_logging(kwargs, |
- {"mach": sys.stdout, |
+ {"tbpl": sys.stdout, |
"raw": log}) |
wptrunner.run_tests(**kwargs) |
with open("raw.log", "rb") as log: |
results, inconsistent = process_results(log, args.iterations) |
+ print >> sys.stderr, "travis_fold:end:running_tests" |
+ |
if results: |
if inconsistent: |
- write_inconsistent(inconsistent) |
- retcode = 1 |
+ write_inconsistent(inconsistent, args.iterations) |
+ retcode = 2 |
else: |
logger.info("All results were stable\n") |
+ print >> sys.stderr, "travis_fold:start:full_results" |
write_results(results, args.iterations) |
+ print >> sys.stderr, "travis_fold:end:full_results" |
else: |
logger.info("No tests run.") |