- add sources.
[platform/framework/web/crosswalk.git] / src / webkit / tools / layout_tests / canary-webkit-revisions.py
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()