| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env python | |
| 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | |
| 3 # Use of this source code is governed by a BSD-style license that can be | |
| 4 # found in the LICENSE file. | |
| 5 | |
| 6 """Retrieve passing and failing WebKit revision numbers from canaries. | |
| 7 | |
| 8 From each canary, | |
| 9 - the last WebKit revision number for which all the tests have passed, | |
| 10 - the last WebKit revision number for which the tests were run, and | |
| 11 - the names of failing layout tests | |
| 12 are retrieved and printed. | |
| 13 """ | |
| 14 | |
| 15 | |
| 16 import json | |
| 17 import optparse | |
| 18 import re | |
| 19 import sys | |
| 20 import urllib2 | |
| 21 | |
| 22 _WEBKIT_REVISION_IN_DEPS_RE = re.compile(r'"webkit_revision"\s*:\s*"(\d+)"') | |
| 23 _DEPS_FILE_URL = "http://src.chromium.org/viewvc/chrome/trunk/src/DEPS" | |
| 24 _DEFAULT_BUILDERS = [ | |
| 25 "Webkit Win", | |
| 26 "Webkit Vista", | |
| 27 "Webkit Win7", | |
| 28 "Webkit Win (dbg)(1)", | |
| 29 "Webkit Win (dbg)(2)", | |
| 30 "Webkit Mac10.5 (CG)", | |
| 31 "Webkit Mac10.6 (CG)", | |
| 32 "Webkit Mac10.5 (CG)(dbg)(1)", | |
| 33 "Webkit Mac10.5 (CG)(dbg)(2)", | |
| 34 "Webkit Mac10.6 (CG)(dbg)", | |
| 35 "Webkit Linux", | |
| 36 "Webkit Linux 32", | |
| 37 "Webkit Linux (dbg)(1)", | |
| 38 "Webkit Linux (dbg)(2)", | |
| 39 ] | |
| 40 _DEFAULT_MAX_BUILDS = 10 | |
| 41 _TEST_PREFIX = "&tests=" | |
| 42 _TEST_SUFFIX = '">' | |
| 43 _WEBKIT_TESTS = "webkit_tests" | |
| 44 | |
| 45 | |
| 46 def _OpenUrl(url): | |
| 47 """Opens a URL. | |
| 48 | |
| 49 Returns: | |
| 50 A file-like object in case of success, an empty list otherwise. | |
| 51 """ | |
| 52 try: | |
| 53 return urllib2.urlopen(url) | |
| 54 except urllib2.URLError, url_error: | |
| 55 message = "" | |
| 56 # Surprisingly, urllib2.URLError has different attributes based on the | |
| 57 # kinds of errors -- "code" for HTTP-level errors, "reason" for others. | |
| 58 if hasattr(url_error, "code"): | |
| 59 message = "Status code: %d" % url_error.code | |
| 60 if hasattr(url_error, "reason"): | |
| 61 message = url_error.reason | |
| 62 print >>sys.stderr, "Failed to open %s: %s" % (url, message) | |
| 63 return [] | |
| 64 | |
| 65 | |
| 66 def _WebkitRevisionInDeps(): | |
| 67 """Returns the WebKit revision specified in DEPS file. | |
| 68 | |
| 69 Returns: | |
| 70 Revision number as int. -1 in case of error. | |
| 71 """ | |
| 72 for line in _OpenUrl(_DEPS_FILE_URL): | |
| 73 match = _WEBKIT_REVISION_IN_DEPS_RE.search(line) | |
| 74 if match: | |
| 75 return int(match.group(1)) | |
| 76 return -1 | |
| 77 | |
| 78 | |
| 79 class _BuildResult(object): | |
| 80 """Build result for a builder. | |
| 81 | |
| 82 Holds builder name, the last passing revision, the last run revision, and | |
| 83 a list of names of failing tests. Revision nubmer 0 is used to represent | |
| 84 that the revision doesn't exist. | |
| 85 """ | |
| 86 def __init__(self, builder, last_passing_revision, last_run_revision, | |
| 87 failing_tests): | |
| 88 """Constructs build results.""" | |
| 89 self.builder = builder | |
| 90 self.last_passing_revision = last_passing_revision | |
| 91 self.last_run_revision = last_run_revision | |
| 92 self.failing_tests = failing_tests | |
| 93 | |
| 94 | |
| 95 def _BuilderUrlFor(builder, max_builds): | |
| 96 """Constructs the URL for a builder to retrieve the last results.""" | |
| 97 url = ("http://build.chromium.org/p/chromium.webkit/json/builders/%s/builds" % | |
| 98 urllib2.quote(builder)) | |
| 99 if max_builds == -1: | |
| 100 return url + "/_all?as_text=1" | |
| 101 return (url + "?as_text=1&" + | |
| 102 '&'.join(["select=%d" % -i for i in range(1, 1 + max_builds)])) | |
| 103 | |
| 104 | |
| 105 def _ExtractFailingTests(build): | |
| 106 """Extracts failing test names from a build result entry JSON object.""" | |
| 107 failing_tests = [] | |
| 108 for step in build["steps"]: | |
| 109 if step["name"] == _WEBKIT_TESTS: | |
| 110 for text in step["text"]: | |
| 111 prefix = text.find(_TEST_PREFIX) | |
| 112 suffix = text.find(_TEST_SUFFIX) | |
| 113 if prefix != -1 and suffix != -1: | |
| 114 failing_tests += sorted( | |
| 115 text[prefix + len(_TEST_PREFIX): suffix].split(",")) | |
| 116 elif "results" in step: | |
| 117 # Existence of "results" entry seems to mean failure. | |
| 118 failing_tests.append(" ".join(step["text"])) | |
| 119 return failing_tests | |
| 120 | |
| 121 | |
| 122 def _RetrieveBuildResult(builder, max_builds, oldest_revision_to_check): | |
| 123 """Retrieves build results for a builder. | |
| 124 | |
| 125 Checks the last passing revision, the last run revision, and failing tests | |
| 126 for the last builds of a builder. | |
| 127 | |
| 128 Args: | |
| 129 builder: Builder name. | |
| 130 max_builds: Maximum number of builds to check. | |
| 131 oldest_revision_to_check: Oldest WebKit revision to check. | |
| 132 | |
| 133 Returns: | |
| 134 _BuildResult instance. | |
| 135 """ | |
| 136 last_run_revision = 0 | |
| 137 failing_tests = [] | |
| 138 succeeded = False | |
| 139 builds_json = _OpenUrl(_BuilderUrlFor(builder, max_builds)) | |
| 140 if not builds_json: | |
| 141 return _BuildResult(builder, 0, 0, failing_tests) | |
| 142 builds = [(int(value["number"]), value) for unused_key, value | |
| 143 in json.loads(''.join(builds_json)).items() | |
| 144 if value.has_key("number")] | |
| 145 builds.sort() | |
| 146 builds.reverse() | |
| 147 for unused_key, build in builds: | |
| 148 if not build.has_key("text"): | |
| 149 continue | |
| 150 if len(build["text"]) < 2: | |
| 151 continue | |
| 152 if not build.has_key("sourceStamp"): | |
| 153 continue | |
| 154 if build["text"][1] == "successful": | |
| 155 succeeded = True | |
| 156 elif not failing_tests: | |
| 157 failing_tests = _ExtractFailingTests(build) | |
| 158 revision = 0 | |
| 159 if build["sourceStamp"]["branch"] == "trunk": | |
| 160 revision = int(build["sourceStamp"]["changes"][-1]["revision"]) | |
| 161 if revision and not last_run_revision: | |
| 162 last_run_revision = revision | |
| 163 if revision and revision < oldest_revision_to_check: | |
| 164 break | |
| 165 if not succeeded or not revision: | |
| 166 continue | |
| 167 return _BuildResult(builder, revision, last_run_revision, failing_tests) | |
| 168 return _BuildResult(builder, 0, last_run_revision, failing_tests) | |
| 169 | |
| 170 | |
| 171 def _PrintPassingRevisions(results, unused_verbose): | |
| 172 """Prints passing revisions and the range of such revisions. | |
| 173 | |
| 174 Args: | |
| 175 results: A list of build results. | |
| 176 """ | |
| 177 print "**** Passing revisions *****" | |
| 178 min_passing_revision = sys.maxint | |
| 179 max_passing_revision = 0 | |
| 180 for result in results: | |
| 181 if result.last_passing_revision: | |
| 182 min_passing_revision = min(min_passing_revision, | |
| 183 result.last_passing_revision) | |
| 184 max_passing_revision = max(max_passing_revision, | |
| 185 result.last_passing_revision) | |
| 186 print 'The last passing run was at r%d on "%s"' % ( | |
| 187 result.last_passing_revision, result.builder) | |
| 188 else: | |
| 189 print 'No passing runs on "%s"' % result.builder | |
| 190 if max_passing_revision: | |
| 191 print "Passing revision range: r%d - r%d" % ( | |
| 192 min_passing_revision, max_passing_revision) | |
| 193 | |
| 194 | |
| 195 def _PrintFailingRevisions(results, verbose): | |
| 196 """Prints failing revisions and the failing tests. | |
| 197 | |
| 198 Args: | |
| 199 results: A list of build results. | |
| 200 """ | |
| 201 failing_test_to_builders = {} | |
| 202 print "**** Failing revisions *****" | |
| 203 for result in results: | |
| 204 if result.last_run_revision and result.failing_tests: | |
| 205 print ('The last run was at r%d on "%s" and the following %d tests' | |
| 206 ' failed' % (result.last_run_revision, result.builder, | |
| 207 len(result.failing_tests))) | |
| 208 for test in result.failing_tests: | |
| 209 print " " + test | |
| 210 failing_test_to_builders.setdefault(test, set()).add(result.builder) | |
| 211 if verbose: | |
| 212 _PrintFailingTestsForBuilderSubsets(failing_test_to_builders) | |
| 213 | |
| 214 | |
| 215 class _FailingTestsForBuilderSubset(object): | |
| 216 def __init__(self, subset_size): | |
| 217 self._subset_size = subset_size | |
| 218 self._tests = [] | |
| 219 | |
| 220 def SubsetSize(self): | |
| 221 return self._subset_size | |
| 222 | |
| 223 def Tests(self): | |
| 224 return self._tests | |
| 225 | |
| 226 | |
| 227 def _PrintFailingTestsForBuilderSubsets(failing_test_to_builders): | |
| 228 """Prints failing test for builder subsets. | |
| 229 | |
| 230 Prints failing tests for each subset of builders, in descending order of the | |
| 231 set size. | |
| 232 """ | |
| 233 print "**** Failing tests ****" | |
| 234 builders_to_tests = {} | |
| 235 for test in failing_test_to_builders: | |
| 236 builders = sorted(failing_test_to_builders[test]) | |
| 237 subset_name = ", ".join(builders) | |
| 238 tests = builders_to_tests.setdefault( | |
| 239 subset_name, _FailingTestsForBuilderSubset(len(builders))).Tests() | |
| 240 tests.append(test) | |
| 241 # Sort subsets in descending order of size and then name. | |
| 242 builder_subsets = [(builders_to_tests[subset_name].SubsetSize(), subset_name) | |
| 243 for subset_name in builders_to_tests] | |
| 244 for subset_size, subset_name in reversed(sorted(builder_subsets)): | |
| 245 print "** Tests failing for %d builders: %s **" % (subset_size, | |
| 246 subset_name) | |
| 247 for test in sorted(builders_to_tests[subset_name].Tests()): | |
| 248 print test | |
| 249 | |
| 250 | |
| 251 def _ParseOptions(): | |
| 252 """Parses command-line options.""" | |
| 253 parser = optparse.OptionParser(usage="%prog [options] [builders]") | |
| 254 parser.add_option("-m", "--max_builds", type="int", | |
| 255 default=-1, | |
| 256 help="Maximum number of builds to check for each builder." | |
| 257 " Defaults to all builds for which record is" | |
| 258 " available. Checking is ended either when the maximum" | |
| 259 " number is reached, the remaining builds are older" | |
| 260 " than the DEPS WebKit revision, or a passing" | |
| 261 " revision is found.") | |
| 262 parser.add_option("-v", "--verbose", action="store_true", default=False, | |
| 263 dest="verbose") | |
| 264 return parser.parse_args() | |
| 265 | |
| 266 | |
| 267 def _Main(): | |
| 268 """The main function.""" | |
| 269 options, builders = _ParseOptions() | |
| 270 if not builders: | |
| 271 builders = _DEFAULT_BUILDERS | |
| 272 oldest_revision_to_check = _WebkitRevisionInDeps() | |
| 273 if options.max_builds == -1 and oldest_revision_to_check == -1: | |
| 274 options.max_builds = _DEFAULT_MAX_BUILDS | |
| 275 if options.max_builds != -1: | |
| 276 print "Maxium number of builds to check: %d" % options.max_builds | |
| 277 if oldest_revision_to_check != -1: | |
| 278 print "Oldest revision to check: %d" % oldest_revision_to_check | |
| 279 sys.stdout.flush() | |
| 280 results = [] | |
| 281 for builder in builders: | |
| 282 print '"%s"' % builder | |
| 283 sys.stdout.flush() | |
| 284 results.append(_RetrieveBuildResult( | |
| 285 builder, options.max_builds, oldest_revision_to_check)) | |
| 286 _PrintFailingRevisions(results, options.verbose) | |
| 287 _PrintPassingRevisions(results, options.verbose) | |
| 288 | |
| 289 | |
| 290 if __name__ == "__main__": | |
| 291 _Main() | |
| OLD | NEW |