-
Notifications
You must be signed in to change notification settings - Fork 3.1k
/
check_stability.py
368 lines (282 loc) · 11.9 KB
/
check_stability.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
from __future__ import print_function
import argparse
import logging
import os
import subprocess
import sys
from ConfigParser import SafeConfigParser
import requests
here = os.path.dirname(__file__)
wpt_root = os.path.abspath(os.path.join(here, os.pardir, os.pardir))
sys.path.insert(0, wpt_root)
from tools.wpt import testfiles
from tools.wpt.testfiles import get_git_cmd
from tools.wpt.virtualenv import Virtualenv
from tools.wpt.utils import Kwargs
from tools.wpt.run import create_parser, setup_wptrunner
from tools.wpt import markdown
from tools import localpaths
logger = None
run, write_inconsistent, write_results = None, None, None
wptrunner = None
def setup_logging():
"""Set up basic debug logger."""
global logger
logger = logging.getLogger(here)
handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter(logging.BASIC_FORMAT, None)
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
def do_delayed_imports():
global run, write_inconsistent, write_results, wptrunner
from tools.wpt.stability import run, write_inconsistent, write_results
from wptrunner import wptrunner
class TravisFold(object):
"""Context for TravisCI folding mechanism. Subclasses object.
See: https://blog.travis-ci.com/2013-05-22-improving-build-visibility-log-folds/
"""
def __init__(self, name):
"""Register TravisCI folding section name."""
self.name = name
def __enter__(self):
"""Emit fold start syntax."""
print("travis_fold:start:%s" % self.name, file=sys.stderr)
def __exit__(self, type, value, traceback):
"""Emit fold end syntax."""
print("travis_fold:end:%s" % self.name, file=sys.stderr)
class FilteredIO(object):
"""Wrap a file object, invoking the provided callback for every call to
`write` and only proceeding with the operation when that callback returns
True."""
def __init__(self, original, on_write):
self.original = original
self.on_write = on_write
def __getattr__(self, name):
return getattr(self.original, name)
def disable(self):
self.write = lambda msg: None
def write(self, msg):
encoded = msg.encode("utf8", "backslashreplace").decode("utf8")
if self.on_write(self.original, encoded) is True:
self.original.write(encoded)
def replace_streams(capacity, warning_msg):
# Value must be boxed to support modification from inner function scope
count = [0]
capacity -= 2 + len(warning_msg)
stderr = sys.stderr
def on_write(handle, msg):
length = len(msg)
count[0] += length
if count[0] > capacity:
wrapped_stdout.disable()
wrapped_stderr.disable()
handle.write(msg[0:capacity - count[0]])
handle.flush()
stderr.write("\n%s\n" % warning_msg)
return False
return True
# Store local references to the replaced streams to guard against the case
# where other code replace the global references.
sys.stdout = wrapped_stdout = FilteredIO(sys.stdout, on_write)
sys.stderr = wrapped_stderr = FilteredIO(sys.stderr, on_write)
def call(*args):
"""Log terminal command, invoke it as a subprocess.
Returns a bytestring of the subprocess output if no error.
"""
logger.debug("%s" % " ".join(args))
try:
return subprocess.check_output(args)
except subprocess.CalledProcessError as e:
logger.critical("%s exited with return code %i" %
(e.cmd, e.returncode))
logger.critical(e.output)
raise
def fetch_wpt(user, *args):
git = get_git_cmd(wpt_root)
git("fetch", "https://github.com/%s/web-platform-tests.git" % user, *args)
def get_sha1():
""" Get and return sha1 of current git branch HEAD commit."""
git = get_git_cmd(wpt_root)
return git("rev-parse", "HEAD").strip()
def deepen_checkout(user):
"""Convert from a shallow checkout to a full one"""
fetch_args = [user, "+refs/heads/*:refs/remotes/origin/*"]
if os.path.exists(os.path.join(wpt_root, ".git", "shallow")):
fetch_args.insert(1, "--unshallow")
fetch_wpt(*fetch_args)
def get_parser():
"""Create and return script-specific argument parser."""
description = """Detect instabilities in new tests by executing tests
repeatedly and comparing results between executions."""
parser = argparse.ArgumentParser(description=description)
parser.add_argument("--user",
action="store",
# Travis docs say do not depend on USER env variable.
# This is a workaround to get what should be the same value
default=os.environ.get("TRAVIS_REPO_SLUG", "w3c").split('/')[0],
help="Travis user name")
parser.add_argument("--output-bytes",
action="store",
type=int,
help="Maximum number of bytes to write to standard output/error")
parser.add_argument("--metadata",
dest="metadata_root",
action="store",
default=wpt_root,
help="Directory that will contain MANIFEST.json")
parser.add_argument("--config-file",
action="store",
type=str,
help="Location of ini-formatted configuration file",
default="check_stability.ini")
parser.add_argument("--rev",
action="store",
default=None,
help="Commit range to use")
return parser
def pr():
pr = os.environ.get("TRAVIS_PULL_REQUEST", "false")
return pr if pr != "false" else None
def post_results(results, pr_number, iterations, product, url, status):
"""Post stability results to a given URL."""
payload_results = []
for test_name, test in results.iteritems():
subtests = []
for subtest_name, subtest in test['subtests'].items():
subtests.append({
'test': subtest_name,
'result': {
'messages': list(subtest['messages']),
'status': subtest['status']
},
})
payload_results.append({
'test': test_name,
'result': {
'status': test['status'],
'subtests': subtests
}
})
payload = {
"pull": {
"number": int(pr_number),
"sha": os.environ.get("TRAVIS_PULL_REQUEST_SHA"),
},
"job": {
"id": int(os.environ.get("TRAVIS_JOB_ID")),
"number": os.environ.get("TRAVIS_JOB_NUMBER"),
"allow_failure": os.environ.get("TRAVIS_ALLOW_FAILURE") == 'true',
"status": status,
},
"build": {
"id": int(os.environ.get("TRAVIS_BUILD_ID")),
"number": os.environ.get("TRAVIS_BUILD_NUMBER"),
},
"product": product,
"iterations": iterations,
"message": "All results were stable." if status == "passed" else "Unstable results.",
"results": payload_results,
}
requests.post(url, json=payload)
def get_changed_files(manifest_path, rev, ignore_changes, skip_tests):
if not rev:
branch_point = testfiles.branch_point()
revish = "%s..HEAD" % branch_point
else:
revish = rev
files_changed, files_ignored = testfiles.files_changed(revish, ignore_changes)
if files_ignored:
logger.info("Ignoring %s changed files:\n%s" %
(len(files_ignored), "".join(" * %s\n" % item for item in files_ignored)))
tests_changed, files_affected = testfiles.affected_testfiles(files_changed, skip_tests,
manifest_path=manifest_path)
return tests_changed, files_affected
def main():
"""Perform check_stability functionality and return exit code."""
venv = Virtualenv(os.environ.get("VIRTUAL_ENV", os.path.join(wpt_root, "_venv")))
venv.install_requirements(os.path.join(wpt_root, "tools", "wptrunner", "requirements.txt"))
venv.install("requests")
args, wpt_args = get_parser().parse_known_args()
return run(venv, wpt_args, **vars(args))
def run(venv, wpt_args, **kwargs):
global logger
do_delayed_imports()
retcode = 0
parser = get_parser()
wpt_args = create_parser().parse_args(wpt_args)
with open(kwargs["config_file"], 'r') as config_fp:
config = SafeConfigParser()
config.readfp(config_fp)
skip_tests = config.get("file detection", "skip_tests").split()
ignore_changes = set(config.get("file detection", "ignore_changes").split())
results_url = config.get("file detection", "results_url")
if kwargs["output_bytes"] is not None:
replace_streams(kwargs["output_bytes"],
"Log reached capacity (%s bytes); output disabled." % kwargs["output_bytes"])
wpt_args.metadata_root = kwargs["metadata_root"]
try:
os.makedirs(wpt_args.metadata_root)
except OSError:
pass
setup_logging()
browser_name = wpt_args.product.split(":")[0]
pr_number = pr()
with TravisFold("browser_setup"):
logger.info(markdown.format_comment_title(wpt_args.product))
if pr is not None:
deepen_checkout(kwargs["user"])
# Ensure we have a branch called "master"
fetch_wpt(kwargs["user"], "master:master")
head_sha1 = get_sha1()
logger.info("Testing web-platform-tests at revision %s" % head_sha1)
wpt_kwargs = Kwargs(vars(wpt_args))
if not wpt_kwargs["test_list"]:
manifest_path = os.path.join(wpt_kwargs["metadata_root"], "MANIFEST.json")
tests_changed, files_affected = get_changed_files(manifest_path, kwargs["rev"],
ignore_changes, skip_tests)
if not (tests_changed or files_affected):
logger.info("No tests changed")
return 0
if tests_changed:
logger.debug("Tests changed:\n%s" % "".join(" * %s\n" % item for item in tests_changed))
if files_affected:
logger.debug("Affected tests:\n%s" % "".join(" * %s\n" % item for item in files_affected))
wpt_kwargs["test_list"] = list(tests_changed | files_affected)
do_delayed_imports()
wpt_kwargs["stability"] = True
wpt_kwargs["prompt"] = False
wpt_kwargs["install_browser"] = True
wpt_kwargs["install"] = wpt_kwargs["product"].split(":")[0] == "firefox"
wpt_kwargs = setup_wptrunner(venv, **wpt_kwargs)
logger.info("Using binary %s" % wpt_kwargs["binary"])
with TravisFold("running_tests"):
logger.info("Starting tests")
wpt_logger = wptrunner.logger
iterations, results, inconsistent = run(venv, wpt_logger, **wpt_kwargs)
if results:
if inconsistent:
write_inconsistent(logger.error, inconsistent, iterations)
retcode = 2
else:
logger.info("All results were stable\n")
with TravisFold("full_results"):
write_results(logger.info, results, iterations,
pr_number=pr_number,
use_details=True)
if pr_number:
post_results(results, iterations=iterations, url=results_url,
product=wpt_args.product, pr_number=pr_number,
status="failed" if inconsistent else "passed")
else:
logger.info("No tests run.")
return retcode
if __name__ == "__main__":
try:
retcode = main()
except Exception:
import traceback
traceback.print_exc()
sys.exit(1)
else:
sys.exit(retcode)