1dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# Copyright (c) 2009, Google Inc. All rights reserved.
2dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block#
3dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# Redistribution and use in source and binary forms, with or without
4dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# modification, are permitted provided that the following conditions are
5dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# met:
6dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block#
7dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block#     * Redistributions of source code must retain the above copyright
8dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# notice, this list of conditions and the following disclaimer.
9dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block#     * Redistributions in binary form must reproduce the above
10dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# copyright notice, this list of conditions and the following disclaimer
11dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# in the documentation and/or other materials provided with the
12dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# distribution.
13dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block#     * Neither the name of Google Inc. nor the names of its
14dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# contributors may be used to endorse or promote products derived from
15dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# this software without specific prior written permission.
16dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block#
17dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block#
29dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block# WebKit's Python module for interacting with WebKit's buildbot
30dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
31f05b935882198ccf7d81675736e3aeb089c5113aBen Murdochtry:
32f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch    import json
33f05b935882198ccf7d81675736e3aeb089c5113aBen Murdochexcept ImportError:
34f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch    # python 2.5 compatibility
35f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch    import webkitpy.thirdparty.simplejson as json
36f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch
37dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Blockimport operator
38dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Blockimport re
39dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Blockimport urllib
40dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Blockimport urllib2
41dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
42bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsenfrom webkitpy.common.net.failuremap import FailureMap
43a94275402997c11dd2e778633dacf4b7e630a35dBen Murdochfrom webkitpy.common.net.layouttestresults import LayoutTestResults
44bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsenfrom webkitpy.common.net.regressionwindow import RegressionWindow
452bde8e466a4451c7319e3a072d118917957d6554Steve Blockfrom webkitpy.common.net.testoutputset import TestOutputSet
46dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Blockfrom webkitpy.common.system.logutils import get_logger
472bde8e466a4451c7319e3a072d118917957d6554Steve Blockfrom webkitpy.common.system.zipfileset import ZipFileSet
48dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Blockfrom webkitpy.thirdparty.BeautifulSoup import BeautifulSoup
492bde8e466a4451c7319e3a072d118917957d6554Steve Blockfrom webkitpy.thirdparty.autoinstalled.mechanize import Browser
50dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
51dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block_log = get_logger(__file__)
52dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
53dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
54dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Blockclass Builder(object):
55dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def __init__(self, name, buildbot):
5621939df44de1705786c545cd1bf519d47250322dBen Murdoch        self._name = name
57dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        self._buildbot = buildbot
58dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        self._builds_cache = {}
59dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        self._revision_to_build_number = None
60dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        self._browser = Browser()
61dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        self._browser.set_handle_robots(False) # The builder pages are excluded by robots.txt
62dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
63dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def name(self):
64dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return self._name
65dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
66dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def results_url(self):
67dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return "http://%s/results/%s" % (self._buildbot.buildbot_host, self.url_encoded_name())
68dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
69dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def url_encoded_name(self):
70dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return urllib.quote(self._name)
71dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
72dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def url(self):
73dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return "http://%s/builders/%s" % (self._buildbot.buildbot_host, self.url_encoded_name())
74dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
75dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    # This provides a single place to mock
76dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def _fetch_build(self, build_number):
77f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch        build_dictionary = self._buildbot._fetch_build_dictionary(self, build_number)
78dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        if not build_dictionary:
79dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            return None
80dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return Build(self,
81dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            build_number=int(build_dictionary['number']),
82f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch            revision=int(build_dictionary['sourceStamp']['revision']),
83f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch            is_green=(build_dictionary['results'] == 0) # Undocumented, 0 seems to mean "pass"
84dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        )
85dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
86dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def build(self, build_number):
87dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        if not build_number:
88dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            return None
89dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        cached_build = self._builds_cache.get(build_number)
90dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        if cached_build:
91dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            return cached_build
92dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
93dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        build = self._fetch_build(build_number)
94dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        self._builds_cache[build_number] = build
95dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return build
96dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
972bde8e466a4451c7319e3a072d118917957d6554Steve Block    def latest_cached_build(self):
982bde8e466a4451c7319e3a072d118917957d6554Steve Block        revision_build_pairs = self.revision_build_pairs_with_results()
992bde8e466a4451c7319e3a072d118917957d6554Steve Block        revision_build_pairs.sort(key=lambda i: i[1])
1002bde8e466a4451c7319e3a072d118917957d6554Steve Block        latest_build_number = revision_build_pairs[-1][1]
1012bde8e466a4451c7319e3a072d118917957d6554Steve Block        return self.build(latest_build_number)
1022bde8e466a4451c7319e3a072d118917957d6554Steve Block
103dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def force_build(self, username="webkit-patch", comments=None):
104dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        def predicate(form):
105dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            try:
106dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                return form.find_control("username")
107dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            except Exception, e:
108dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                return False
109dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        self._browser.open(self.url())
110dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        self._browser.select_form(predicate=predicate)
111dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        self._browser["username"] = username
112dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        if comments:
113dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            self._browser["comments"] = comments
114dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return self._browser.submit()
115dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
116dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    file_name_regexp = re.compile(r"r(?P<revision>\d+) \((?P<build_number>\d+)\)")
117dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def _revision_and_build_for_filename(self, filename):
118dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        # Example: "r47483 (1)/" or "r47483 (1).zip"
119dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        match = self.file_name_regexp.match(filename)
120dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return (int(match.group("revision")), int(match.group("build_number")))
121dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
122dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def _fetch_revision_to_build_map(self):
123dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        # All _fetch requests go through _buildbot for easier mocking
124a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch        # FIXME: This should use NetworkTransaction's 404 handling instead.
125dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        try:
126dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            # FIXME: This method is horribly slow due to the huge network load.
127dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            # FIXME: This is a poor way to do revision -> build mapping.
128dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            # Better would be to ask buildbot through some sort of API.
129dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            print "Loading revision/build list from %s." % self.results_url()
130dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            print "This may take a while..."
131dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            result_files = self._buildbot._fetch_twisted_directory_listing(self.results_url())
132dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        except urllib2.HTTPError, error:
133dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            if error.code != 404:
134dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                raise
135dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            result_files = []
136dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
137dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        # This assumes there was only one build per revision, which is false but we don't care for now.
138dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return dict([self._revision_and_build_for_filename(file_info["filename"]) for file_info in result_files])
139dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
140dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def _revision_to_build_map(self):
141dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        if not self._revision_to_build_number:
142dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            self._revision_to_build_number = self._fetch_revision_to_build_map()
143dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return self._revision_to_build_number
144dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
145dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def revision_build_pairs_with_results(self):
146dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return self._revision_to_build_map().items()
147dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
148dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    # This assumes there can be only one build per revision, which is false, but we don't care for now.
149dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def build_for_revision(self, revision, allow_failed_lookups=False):
150dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        # NOTE: This lookup will fail if that exact revision was never built.
151dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        build_number = self._revision_to_build_map().get(int(revision))
152dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        if not build_number:
153dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            return None
154dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        build = self.build(build_number)
155dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        if not build and allow_failed_lookups:
156f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch            # Builds for old revisions with fail to lookup via buildbot's json api.
157dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            build = Build(self,
158dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                build_number=build_number,
159dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                revision=revision,
160dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                is_green=False,
161dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            )
162dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return build
163dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
164bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen    def find_regression_window(self, red_build, look_back_limit=30):
165dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        if not red_build or red_build.is_green():
166bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen            return RegressionWindow(None, None)
167dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        common_failures = None
168dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        current_build = red_build
169dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        build_after_current_build = None
170dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        look_back_count = 0
171dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        while current_build:
172dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            if current_build.is_green():
173dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                # current_build can't possibly have any failures in common
174dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                # with red_build because it's green.
175dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                break
176dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            results = current_build.layout_test_results()
177dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            # We treat a lack of results as if all the test failed.
178dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            # This occurs, for example, when we can't compile at all.
179dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            if results:
180dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                failures = set(results.failing_tests())
181dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                if common_failures == None:
182dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                    common_failures = failures
183a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch                else:
184a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch                    common_failures = common_failures.intersection(failures)
185a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch                    if not common_failures:
186a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch                        # current_build doesn't have any failures in common with
187a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch                        # the red build we're worried about.  We assume that any
188a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch                        # failures in current_build were due to flakiness.
189a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch                        break
190dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            look_back_count += 1
191dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            if look_back_count > look_back_limit:
192a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch                return RegressionWindow(None, current_build, failing_tests=common_failures)
193dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            build_after_current_build = current_build
194dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            current_build = current_build.previous_build()
195dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        # We must iterate at least once because red_build is red.
196dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        assert(build_after_current_build)
197dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        # Current build must either be green or have no failures in common
198dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        # with red build, so we've found our failure transition.
199a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch        return RegressionWindow(current_build, build_after_current_build, failing_tests=common_failures)
200dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
201bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen    def find_blameworthy_regression_window(self, red_build_number, look_back_limit=30, avoid_flakey_tests=True):
202dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        red_build = self.build(red_build_number)
203bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen        regression_window = self.find_regression_window(red_build, look_back_limit)
204bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen        if not regression_window.build_before_failure():
205bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen            return None  # We ran off the limit of our search
206dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        # If avoid_flakey_tests, require at least 2 bad builds before we
207dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        # suspect a real failure transition.
208bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen        if avoid_flakey_tests and regression_window.failing_build() == red_build:
209bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen            return None
210bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen        return regression_window
211dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
212dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
213dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Blockclass Build(object):
214dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def __init__(self, builder, build_number, revision, is_green):
215dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        self._builder = builder
216dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        self._number = build_number
217dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        self._revision = revision
218dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        self._is_green = is_green
219dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        self._layout_test_results = None
220dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
221dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    @staticmethod
222dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def build_url(builder, build_number):
223dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return "%s/builds/%s" % (builder.url(), build_number)
224dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
225dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def url(self):
226dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return self.build_url(self.builder(), self._number)
227dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
228dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def results_url(self):
229dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        results_directory = "r%s (%s)" % (self.revision(), self._number)
230dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return "%s/%s" % (self._builder.results_url(), urllib.quote(results_directory))
231dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
2322bde8e466a4451c7319e3a072d118917957d6554Steve Block    def results_zip_url(self):
2332bde8e466a4451c7319e3a072d118917957d6554Steve Block        return "%s.zip" % self.results_url()
2342bde8e466a4451c7319e3a072d118917957d6554Steve Block
2352bde8e466a4451c7319e3a072d118917957d6554Steve Block    def results(self):
2362bde8e466a4451c7319e3a072d118917957d6554Steve Block        return TestOutputSet(self._builder.name(), None, ZipFileSet(self.results_zip_url()), include_expected=False)
2372bde8e466a4451c7319e3a072d118917957d6554Steve Block
238a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch    def _fetch_results_html(self):
239a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch        results_html = "%s/results.html" % (self.results_url())
240a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch        # FIXME: This should use NetworkTransaction's 404 handling instead.
241a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch        try:
2426b70adc33054f8aee8c54d0f460458a9df11b8a5Russell Brenner            # It seems this can return None if the url redirects and then returns 404.
243a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch            return urllib2.urlopen(results_html)
244a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch        except urllib2.HTTPError, error:
245a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch            if error.code != 404:
246a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch                raise
247a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch
248dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def layout_test_results(self):
249dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        if not self._layout_test_results:
250a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch            # FIXME: This should cache that the result was a 404 and stop hitting the network.
251a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch            self._layout_test_results = LayoutTestResults.results_from_string(self._fetch_results_html())
252dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return self._layout_test_results
253dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
254dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def builder(self):
255dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return self._builder
256dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
257dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def revision(self):
258dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return self._revision
259dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
260dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def is_green(self):
261dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return self._is_green
262dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
263dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def previous_build(self):
264dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        # previous_build() allows callers to avoid assuming build numbers are sequential.
265dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        # They may not be sequential across all master changes, or when non-trunk builds are made.
266dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return self._builder.build(self._number - 1)
267dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
268dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
269dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Blockclass BuildBot(object):
2702daae5fd11344eaa88a0d92b0f6d65f8d2255c00Ben Murdoch    # FIXME: This should move into common.config.urls.
271dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    default_host = "build.webkit.org"
272dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
273dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def __init__(self, host=default_host):
274dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        self.buildbot_host = host
275dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        self._builder_by_name = {}
276dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
277dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        # If any core builder is red we should not be landing patches.  Other
278dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        # builders should be added to this list once they are known to be
279dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        # reliable.
280dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        # See https://bugs.webkit.org/show_bug.cgi?id=33296 and related bugs.
281dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        self.core_builder_names_regexps = [
282dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            "SnowLeopard.*Build",
2832fc2651226baac27029e38c9d6ef883fa32084dbSteve Block            "SnowLeopard.*\(Test",
2842fc2651226baac27029e38c9d6ef883fa32084dbSteve Block            "SnowLeopard.*\(WebKit2 Test",
2852bde8e466a4451c7319e3a072d118917957d6554Steve Block            "Leopard.*",
286dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            "Windows.*Build",
2872bde8e466a4451c7319e3a072d118917957d6554Steve Block            "Windows.*\(Test",
2882bde8e466a4451c7319e3a072d118917957d6554Steve Block            "WinCairo",
28981bc750723a18f21cd17d1b173cd2a4dda9cea6eBen Murdoch            "WinCE",
2902fc2651226baac27029e38c9d6ef883fa32084dbSteve Block            "EFL",
29106ea8e899e48f1f2f396b70e63fae369f2f23232Kristian Monsen            "GTK.*32",
29206ea8e899e48f1f2f396b70e63fae369f2f23232Kristian Monsen            "GTK.*64.*Debug",  # Disallow the 64-bit Release bot which is broken.
293dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            "Qt",
29406ea8e899e48f1f2f396b70e63fae369f2f23232Kristian Monsen            "Chromium.*Release$",
295dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        ]
296dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
297dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def _parse_last_build_cell(self, builder, cell):
298dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        status_link = cell.find('a')
299dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        if status_link:
300dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            # Will be either a revision number or a build number
301dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            revision_string = status_link.string
302dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            # If revision_string has non-digits assume it's not a revision number.
303dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            builder['built_revision'] = int(revision_string) \
304dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                                        if not re.match('\D', revision_string) \
305dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                                        else None
306545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch
307545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch            # FIXME: We treat slave lost as green even though it is not to
308545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch            # work around the Qts bot being on a broken internet connection.
309545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch            # The real fix is https://bugs.webkit.org/show_bug.cgi?id=37099
310545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch            builder['is_green'] = not re.search('fail', cell.renderContents()) or \
311545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch                                  not not re.search('lost', cell.renderContents())
312dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
313dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            status_link_regexp = r"builders/(?P<builder_name>.*)/builds/(?P<build_number>\d+)"
314dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            link_match = re.match(status_link_regexp, status_link['href'])
315dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            builder['build_number'] = int(link_match.group("build_number"))
316dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        else:
317dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            # We failed to find a link in the first cell, just give up.  This
318dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            # can happen if a builder is just-added, the first cell will just
319dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            # be "no build"
320dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            # Other parts of the code depend on is_green being present.
321dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            builder['is_green'] = False
322dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            builder['built_revision'] = None
323dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            builder['build_number'] = None
324dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
325dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def _parse_current_build_cell(self, builder, cell):
326dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        activity_lines = cell.renderContents().split("<br />")
327dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        builder["activity"] = activity_lines[0] # normally "building" or "idle"
328dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        # The middle lines document how long left for any current builds.
329dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        match = re.match("(?P<pending_builds>\d) pending", activity_lines[-1])
330dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        builder["pending_builds"] = int(match.group("pending_builds")) if match else 0
331dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
332dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def _parse_builder_status_from_row(self, status_row):
333dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        status_cells = status_row.findAll('td')
334dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        builder = {}
335dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
336dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        # First cell is the name
337dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        name_link = status_cells[0].find('a')
33821939df44de1705786c545cd1bf519d47250322dBen Murdoch        builder["name"] = unicode(name_link.string)
339dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
340dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        self._parse_last_build_cell(builder, status_cells[1])
341dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        self._parse_current_build_cell(builder, status_cells[2])
342dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return builder
343dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
344dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def _matches_regexps(self, builder_name, name_regexps):
345dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        for name_regexp in name_regexps:
346dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            if re.match(name_regexp, builder_name):
347dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                return True
348dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return False
349dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
350dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    # FIXME: Should move onto Builder
351dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def _is_core_builder(self, builder_name):
352dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return self._matches_regexps(builder_name, self.core_builder_names_regexps)
353dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
354dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    # FIXME: This method needs to die, but is used by a unit test at the moment.
355dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def _builder_statuses_with_names_matching_regexps(self, builder_statuses, name_regexps):
356dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return [builder for builder in builder_statuses if self._matches_regexps(builder["name"], name_regexps)]
357dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
358dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def red_core_builders(self):
359dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return [builder for builder in self.core_builder_statuses() if not builder["is_green"]]
360dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
361dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def red_core_builders_names(self):
362dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return [builder["name"] for builder in self.red_core_builders()]
363dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
364dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def idle_red_core_builders(self):
365dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return [builder for builder in self.red_core_builders() if builder["activity"] == "idle"]
366dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
367dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def core_builders_are_green(self):
368dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return not self.red_core_builders()
369dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
370dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    # FIXME: These _fetch methods should move to a networking class.
371f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch    def _fetch_build_dictionary(self, builder, build_number):
372dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        try:
373f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch            base = "http://%s" % self.buildbot_host
374f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch            path = urllib.quote("json/builders/%s/builds/%s" % (builder.name(),
375f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch                                                                build_number))
376f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch            url = "%s/%s" % (base, path)
377f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch            jsondata = urllib2.urlopen(url)
378f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch            return json.load(jsondata)
379f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch        except urllib2.URLError, err:
380dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            build_url = Build.build_url(builder, build_number)
381dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            _log.error("Error fetching data for %s build %s (%s): %s" % (builder.name(), build_number, build_url, err))
382dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            return None
383f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch        except ValueError, err:
384f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch            build_url = Build.build_url(builder, build_number)
385f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch            _log.error("Error decoding json data from %s: %s" % (build_url, err))
386f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch            return None
387dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
388dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def _fetch_one_box_per_builder(self):
389dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        build_status_url = "http://%s/one_box_per_builder" % self.buildbot_host
390dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return urllib2.urlopen(build_status_url)
391dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
392bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen    def _file_cell_text(self, file_cell):
393bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen        """Traverses down through firstChild elements until one containing a string is found, then returns that string"""
394bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen        element = file_cell
395bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen        while element.string is None and element.contents:
396bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen            element = element.contents[0]
397bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen        return element.string
398bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen
399dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def _parse_twisted_file_row(self, file_row):
400bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen        string_or_empty = lambda string: unicode(string) if string else u""
401dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        file_cells = file_row.findAll('td')
402dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return {
403bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen            "filename": string_or_empty(self._file_cell_text(file_cells[0])),
404bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen            "size": string_or_empty(self._file_cell_text(file_cells[1])),
405bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen            "type": string_or_empty(self._file_cell_text(file_cells[2])),
406bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen            "encoding": string_or_empty(self._file_cell_text(file_cells[3])),
407dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        }
408dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
409dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def _parse_twisted_directory_listing(self, page):
410dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        soup = BeautifulSoup(page)
411dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        # HACK: Match only table rows with a class to ignore twisted header/footer rows.
412bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen        file_rows = soup.find('table').findAll('tr', {'class': re.compile(r'\b(?:directory|file)\b')})
413dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return [self._parse_twisted_file_row(file_row) for file_row in file_rows]
414dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
415dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    # FIXME: There should be a better way to get this information directly from twisted.
416dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def _fetch_twisted_directory_listing(self, url):
417dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return self._parse_twisted_directory_listing(urllib2.urlopen(url))
418dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
419dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def builders(self):
420dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return [self.builder_with_name(status["name"]) for status in self.builder_statuses()]
421dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
422dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    # This method pulls from /one_box_per_builder as an efficient way to get information about
423dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def builder_statuses(self):
424dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        soup = BeautifulSoup(self._fetch_one_box_per_builder())
425dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return [self._parse_builder_status_from_row(status_row) for status_row in soup.find('table').findAll('tr')]
426dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
427dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def core_builder_statuses(self):
428dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return [builder for builder in self.builder_statuses() if self._is_core_builder(builder["name"])]
429dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
430dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def builder_with_name(self, name):
431dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        builder = self._builder_by_name.get(name)
432dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        if not builder:
433dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            builder = Builder(name, self)
434dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            self._builder_by_name[name] = builder
435dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return builder
436dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
437bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen    def failure_map(self, only_core_builders=True):
438dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        builder_statuses = self.core_builder_statuses() if only_core_builders else self.builder_statuses()
439bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen        failure_map = FailureMap()
440dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        revision_to_failing_bots = {}
441dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        for builder_status in builder_statuses:
442dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            if builder_status["is_green"]:
443dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                continue
444dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            builder = self.builder_with_name(builder_status["name"])
445bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen            regression_window = builder.find_blameworthy_regression_window(builder_status["build_number"])
446a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch            if regression_window:
447a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch                failure_map.add_regression_window(builder, regression_window)
448bec39347bb3bb5bf1187ccaf471d26247f28b585Kristian Monsen        return failure_map
449dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
450dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    # This makes fewer requests than calling Builder.latest_build would.  It grabs all builder
451dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    # statuses in one request using self.builder_statuses (fetching /one_box_per_builder instead of builder pages).
452dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def _latest_builds_from_builders(self, only_core_builders=True):
453dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        builder_statuses = self.core_builder_statuses() if only_core_builders else self.builder_statuses()
454dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        return [self.builder_with_name(status["name"]).build(status["build_number"]) for status in builder_statuses]
455dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
456dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def _build_at_or_before_revision(self, build, revision):
457dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        while build:
458dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            if build.revision() <= revision:
459dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                return build
460dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            build = build.previous_build()
461dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block
462dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block    def last_green_revision(self, only_core_builders=True):
463dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        builds = self._latest_builds_from_builders(only_core_builders)
464dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        target_revision = builds[0].revision()
465dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        # An alternate way to do this would be to start at one revision and walk backwards
466dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        # checking builder.build_for_revision, however build_for_revision is very slow on first load.
467dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block        while True:
468dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            # Make builds agree on revision
469dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            builds = [self._build_at_or_before_revision(build, target_revision) for build in builds]
470dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            if None in builds: # One of the builds failed to load from the server.
471dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                return None
472dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            min_revision = min(map(lambda build: build.revision(), builds))
473dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            if min_revision != target_revision:
474dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                target_revision = min_revision
475dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                continue # Builds don't all agree on revision, keep searching
476dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            # Check to make sure they're all green
477dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            all_are_green = reduce(operator.and_, map(lambda build: build.is_green(), builds))
478dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            if not all_are_green:
479dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                target_revision -= 1
480dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block                continue
481dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block            return min_revision
482