summaryrefslogtreecommitdiffstats
path: root/WebKitTools/Scripts/webkitpy/common/net
diff options
context:
space:
mode:
Diffstat (limited to 'WebKitTools/Scripts/webkitpy/common/net')
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/bugzilla.py7
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/bugzilla_unittest.py15
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/buildbot.py95
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py46
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/failuremap.py54
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/failuremap_unittest.py76
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/layouttestresults.py88
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/layouttestresults_unittest.py76
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/networktransaction.py10
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/networktransaction_unittest.py15
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/regressionwindow.py17
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/statusserver.py70
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/statusserver_unittest.py43
13 files changed, 436 insertions, 176 deletions
diff --git a/WebKitTools/Scripts/webkitpy/common/net/bugzilla.py b/WebKitTools/Scripts/webkitpy/common/net/bugzilla.py
index cc64fac..94519a7 100644
--- a/WebKitTools/Scripts/webkitpy/common/net/bugzilla.py
+++ b/WebKitTools/Scripts/webkitpy/common/net/bugzilla.py
@@ -301,15 +301,14 @@ class BugzillaQueries(object):
review_queue_url = "buglist.cgi?query_format=advanced&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&field0-0-0=flagtypes.name&type0-0-0=equals&value0-0-0=review?"
return self._fetch_bug_ids_advanced_query(review_queue_url)
+ # This method will make several requests to bugzilla.
def fetch_patches_from_review_queue(self, limit=None):
# [:None] returns the whole array.
return sum([self._fetch_bug(bug_id).unreviewed_patches()
for bug_id in self._fetch_bug_ids_from_review_queue()[:limit]], [])
- # FIXME: Why do we have both fetch_patches_from_review_queue and
- # fetch_attachment_ids_from_review_queue??
- # NOTE: This is also the only client of _fetch_attachment_ids_request_query
-
+ # NOTE: This is the only client of _fetch_attachment_ids_request_query
+ # This method only makes one request to bugzilla.
def fetch_attachment_ids_from_review_queue(self):
review_queue_url = "request.cgi?action=queue&type=review&group=type"
return self._fetch_attachment_ids_request_query(review_queue_url)
diff --git a/WebKitTools/Scripts/webkitpy/common/net/bugzilla_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/bugzilla_unittest.py
index 32f23cd..3a454d6 100644
--- a/WebKitTools/Scripts/webkitpy/common/net/bugzilla_unittest.py
+++ b/WebKitTools/Scripts/webkitpy/common/net/bugzilla_unittest.py
@@ -33,24 +33,11 @@ import datetime
from webkitpy.common.config.committers import CommitterList, Reviewer, Committer
from webkitpy.common.net.bugzilla import Bugzilla, BugzillaQueries, parse_bug_id, CommitterValidator, Bug
from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.tool.mocktool import MockBrowser
from webkitpy.thirdparty.mock import Mock
from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup
-class MockBrowser(object):
- def open(self, url):
- pass
-
- def select_form(self, name):
- pass
-
- def __setitem__(self, key, value):
- pass
-
- def submit(self):
- pass
-
-
class BugTest(unittest.TestCase):
def test_is_unassigned(self):
for email in Bug.unassigned_emails:
diff --git a/WebKitTools/Scripts/webkitpy/common/net/buildbot.py b/WebKitTools/Scripts/webkitpy/common/net/buildbot.py
index 17f6c7a..a14bc7f 100644
--- a/WebKitTools/Scripts/webkitpy/common/net/buildbot.py
+++ b/WebKitTools/Scripts/webkitpy/common/net/buildbot.py
@@ -35,12 +35,12 @@ import urllib2
import xmlrpclib
from webkitpy.common.net.failuremap import FailureMap
+from webkitpy.common.net.layouttestresults import LayoutTestResults
from webkitpy.common.net.regressionwindow import RegressionWindow
from webkitpy.common.system.logutils import get_logger
from webkitpy.thirdparty.autoinstalled.mechanize import Browser
from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup
-
_log = get_logger(__file__)
@@ -108,6 +108,7 @@ class Builder(object):
def _fetch_revision_to_build_map(self):
# All _fetch requests go through _buildbot for easier mocking
+ # FIXME: This should use NetworkTransaction's 404 handling instead.
try:
# FIXME: This method is horribly slow due to the huge network load.
# FIXME: This is a poor way to do revision -> build mapping.
@@ -166,22 +167,23 @@ class Builder(object):
failures = set(results.failing_tests())
if common_failures == None:
common_failures = failures
- common_failures = common_failures.intersection(failures)
- if not common_failures:
- # current_build doesn't have any failures in common with
- # the red build we're worried about. We assume that any
- # failures in current_build were due to flakiness.
- break
+ else:
+ common_failures = common_failures.intersection(failures)
+ if not common_failures:
+ # current_build doesn't have any failures in common with
+ # the red build we're worried about. We assume that any
+ # failures in current_build were due to flakiness.
+ break
look_back_count += 1
if look_back_count > look_back_limit:
- return RegressionWindow(None, current_build, common_failures=common_failures)
+ return RegressionWindow(None, current_build, failing_tests=common_failures)
build_after_current_build = current_build
current_build = current_build.previous_build()
# We must iterate at least once because red_build is red.
assert(build_after_current_build)
# Current build must either be green or have no failures in common
# with red build, so we've found our failure transition.
- return RegressionWindow(current_build, build_after_current_build, common_failures=common_failures)
+ return RegressionWindow(current_build, build_after_current_build, failing_tests=common_failures)
def find_blameworthy_regression_window(self, red_build_number, look_back_limit=30, avoid_flakey_tests=True):
red_build = self.build(red_build_number)
@@ -195,66 +197,6 @@ class Builder(object):
return regression_window
-# FIXME: This should be unified with all the layout test results code in the layout_tests package
-class LayoutTestResults(object):
- stderr_key = u'Tests that had stderr output:'
- fail_key = u'Tests where results did not match expected results:'
- timeout_key = u'Tests that timed out:'
- crash_key = u'Tests that caused the DumpRenderTree tool to crash:'
- missing_key = u'Tests that had no expected results (probably new):'
-
- expected_keys = [
- stderr_key,
- fail_key,
- crash_key,
- timeout_key,
- missing_key,
- ]
-
- @classmethod
- def _parse_results_html(cls, page):
- parsed_results = {}
- tables = BeautifulSoup(page).findAll("table")
- for table in tables:
- table_title = unicode(table.findPreviousSibling("p").string)
- if table_title not in cls.expected_keys:
- # This Exception should only ever be hit if run-webkit-tests changes its results.html format.
- raise Exception("Unhandled title: %s" % table_title)
- # We might want to translate table titles into identifiers before storing.
- parsed_results[table_title] = [unicode(row.find("a").string) for row in table.findAll("tr")]
-
- return parsed_results
-
- @classmethod
- def _fetch_results_html(cls, base_url):
- results_html = "%s/results.html" % base_url
- # FIXME: We need to move this sort of 404 logic into NetworkTransaction or similar.
- try:
- page = urllib2.urlopen(results_html)
- return cls._parse_results_html(page)
- except urllib2.HTTPError, error:
- if error.code != 404:
- raise
-
- @classmethod
- def results_from_url(cls, base_url):
- parsed_results = cls._fetch_results_html(base_url)
- if not parsed_results:
- return None
- return cls(base_url, parsed_results)
-
- def __init__(self, base_url, parsed_results):
- self._base_url = base_url
- self._parsed_results = parsed_results
-
- def parsed_results(self):
- return self._parsed_results
-
- def failing_tests(self):
- failing_keys = [self.fail_key, self.crash_key, self.timeout_key]
- return sorted(sum([tests for key, tests in self._parsed_results.items() if key in failing_keys], []))
-
-
class Build(object):
def __init__(self, builder, build_number, revision, is_green):
self._builder = builder
@@ -274,9 +216,19 @@ class Build(object):
results_directory = "r%s (%s)" % (self.revision(), self._number)
return "%s/%s" % (self._builder.results_url(), urllib.quote(results_directory))
+ def _fetch_results_html(self):
+ results_html = "%s/results.html" % (self.results_url())
+ # FIXME: This should use NetworkTransaction's 404 handling instead.
+ try:
+ return urllib2.urlopen(results_html)
+ except urllib2.HTTPError, error:
+ if error.code != 404:
+ raise
+
def layout_test_results(self):
if not self._layout_test_results:
- self._layout_test_results = LayoutTestResults.results_from_url(self.results_url())
+ # FIXME: This should cache that the result was a 404 and stop hitting the network.
+ self._layout_test_results = LayoutTestResults.results_from_string(self._fetch_results_html())
return self._layout_test_results
def builder(self):
@@ -461,7 +413,8 @@ class BuildBot(object):
continue
builder = self.builder_with_name(builder_status["name"])
regression_window = builder.find_blameworthy_regression_window(builder_status["build_number"])
- failure_map.add_regression_window(builder, regression_window)
+ if regression_window:
+ failure_map.add_regression_window(builder, regression_window)
return failure_map
# This makes fewer requests than calling Builder.latest_build would. It grabs all builder
diff --git a/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py
index c99ab32..afc9a39 100644
--- a/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py
+++ b/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py
@@ -29,7 +29,6 @@
import unittest
from webkitpy.common.net.buildbot import BuildBot, Builder, Build, LayoutTestResults
-
from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup
@@ -42,10 +41,8 @@ class BuilderTest(unittest.TestCase):
revision=build_number + 1000,
is_green=build_number < 4
)
- build._layout_test_results = LayoutTestResults(
- "http://buildbot.example.com/foo", {
- LayoutTestResults.fail_key: failure(build_number),
- })
+ parsed_results = {LayoutTestResults.fail_key: failure(build_number)}
+ build._layout_test_results = LayoutTestResults(parsed_results)
return build
self.builder._fetch_build = _mock_fetch_build
@@ -114,45 +111,6 @@ class BuilderTest(unittest.TestCase):
self.assertEqual(self.builder._revision_and_build_for_filename(filename), revision_and_build)
-class LayoutTestResultsTest(unittest.TestCase):
- _example_results_html = """
-<html>
-<head>
-<title>Layout Test Results</title>
-</head>
-<body>
-<p>Tests that had stderr output:</p>
-<table>
-<tr>
-<td><a href="/var/lib/buildbot/build/gtk-linux-64-release/build/LayoutTests/accessibility/aria-activedescendant-crash.html">accessibility/aria-activedescendant-crash.html</a></td>
-<td><a href="accessibility/aria-activedescendant-crash-stderr.txt">stderr</a></td>
-</tr>
-<td><a href="/var/lib/buildbot/build/gtk-linux-64-release/build/LayoutTests/http/tests/security/canvas-remote-read-svg-image.html">http/tests/security/canvas-remote-read-svg-image.html</a></td>
-<td><a href="http/tests/security/canvas-remote-read-svg-image-stderr.txt">stderr</a></td>
-</tr>
-</table><p>Tests that had no expected results (probably new):</p>
-<table>
-<tr>
-<td><a href="/var/lib/buildbot/build/gtk-linux-64-release/build/LayoutTests/fast/repaint/no-caret-repaint-in-non-content-editable-element.html">fast/repaint/no-caret-repaint-in-non-content-editable-element.html</a></td>
-<td><a href="fast/repaint/no-caret-repaint-in-non-content-editable-element-actual.txt">result</a></td>
-</tr>
-</table></body>
-</html>
-"""
-
- _expected_layout_test_results = {
- 'Tests that had stderr output:' : [
- 'accessibility/aria-activedescendant-crash.html'
- ],
- 'Tests that had no expected results (probably new):' : [
- 'fast/repaint/no-caret-repaint-in-non-content-editable-element.html'
- ]
- }
- def test_parse_layout_test_results(self):
- results = LayoutTestResults._parse_results_html(self._example_results_html)
- self.assertEqual(self._expected_layout_test_results, results)
-
-
class BuildBotTest(unittest.TestCase):
_example_one_box_status = '''
diff --git a/WebKitTools/Scripts/webkitpy/common/net/failuremap.py b/WebKitTools/Scripts/webkitpy/common/net/failuremap.py
index 98e4b8f..e2d53ae 100644
--- a/WebKitTools/Scripts/webkitpy/common/net/failuremap.py
+++ b/WebKitTools/Scripts/webkitpy/common/net/failuremap.py
@@ -37,12 +37,48 @@ class FailureMap(object):
'regression_window': regression_window,
})
- def revisions_causing_failures(self):
- revision_to_failing_bots = {}
- for failure_info in self._failures:
- revisions = failure_info['regression_window'].revisions()
- for revision in revisions:
- failing_bots = revision_to_failing_bots.get(revision, [])
- failing_bots.append(failure_info['builder'])
- revision_to_failing_bots[revision] = failing_bots
- return revision_to_failing_bots
+ def is_empty(self):
+ return not self._failures
+
+ def failing_revisions(self):
+ failing_revisions = [failure_info['regression_window'].revisions()
+ for failure_info in self._failures]
+ return sorted(set(sum(failing_revisions, [])))
+
+ def builders_failing_for(self, revision):
+ return self._builders_failing_because_of([revision])
+
+ def tests_failing_for(self, revision):
+ tests = [failure_info['regression_window'].failing_tests()
+ for failure_info in self._failures
+ if revision in failure_info['regression_window'].revisions()
+ and failure_info['regression_window'].failing_tests()]
+ result = set()
+ for test in tests:
+ result = result.union(test)
+ return sorted(result)
+
+ def _old_failures(self, is_old_failure):
+ return filter(lambda revision: is_old_failure(revision),
+ self.failing_revisions())
+
+ def _builders_failing_because_of(self, revisions):
+ revision_set = set(revisions)
+ return [failure_info['builder'] for failure_info in self._failures
+ if revision_set.intersection(
+ failure_info['regression_window'].revisions())]
+
+ # FIXME: We should re-process old failures after some time delay.
+ # https://bugs.webkit.org/show_bug.cgi?id=36581
+ def filter_out_old_failures(self, is_old_failure):
+ old_failures = self._old_failures(is_old_failure)
+ old_failing_builder_names = set([builder.name()
+ for builder in self._builders_failing_because_of(old_failures)])
+
+ # We filter out all the failing builders that could have been caused
+ # by old_failures. We could miss some new failures this way, but
+ # emperically, this reduces the amount of spam we generate.
+ failures = self._failures
+ self._failures = [failure_info for failure_info in failures
+ if failure_info['builder'].name() not in old_failing_builder_names]
+ self._cache = {}
diff --git a/WebKitTools/Scripts/webkitpy/common/net/failuremap_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/failuremap_unittest.py
new file mode 100644
index 0000000..2f0b49d
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/common/net/failuremap_unittest.py
@@ -0,0 +1,76 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.common.net.buildbot import Build
+from webkitpy.common.net.failuremap import *
+from webkitpy.common.net.regressionwindow import RegressionWindow
+from webkitpy.tool.mocktool import MockBuilder
+
+
+class FailureMapTest(unittest.TestCase):
+ builder1 = MockBuilder("Builder1")
+ builder2 = MockBuilder("Builder2")
+
+ build1a = Build(builder1, build_number=22, revision=1233, is_green=True)
+ build1b = Build(builder1, build_number=23, revision=1234, is_green=False)
+ build2a = Build(builder2, build_number=89, revision=1233, is_green=True)
+ build2b = Build(builder2, build_number=90, revision=1235, is_green=False)
+
+ regression_window1 = RegressionWindow(build1a, build1b, failing_tests=[u'test1', u'test1'])
+ regression_window2 = RegressionWindow(build2a, build2b, failing_tests=[u'test1'])
+
+ def _make_failure_map(self):
+ failure_map = FailureMap()
+ failure_map.add_regression_window(self.builder1, self.regression_window1)
+ failure_map.add_regression_window(self.builder2, self.regression_window2)
+ return failure_map
+
+ def test_failing_revisions(self):
+ failure_map = self._make_failure_map()
+ self.assertEquals(failure_map.failing_revisions(), [1234, 1235])
+
+ def test_new_failures(self):
+ failure_map = self._make_failure_map()
+ failure_map.filter_out_old_failures(lambda revision: False)
+ self.assertEquals(failure_map.failing_revisions(), [1234, 1235])
+
+ def test_new_failures_with_old_revisions(self):
+ failure_map = self._make_failure_map()
+ failure_map.filter_out_old_failures(lambda revision: revision == 1234)
+ self.assertEquals(failure_map.failing_revisions(), [])
+
+ def test_new_failures_with_more_old_revisions(self):
+ failure_map = self._make_failure_map()
+ failure_map.filter_out_old_failures(lambda revision: revision == 1235)
+ self.assertEquals(failure_map.failing_revisions(), [1234])
+
+ def test_tests_failing_for(self):
+ failure_map = self._make_failure_map()
+ self.assertEquals(failure_map.tests_failing_for(1234), [u'test1'])
diff --git a/WebKitTools/Scripts/webkitpy/common/net/layouttestresults.py b/WebKitTools/Scripts/webkitpy/common/net/layouttestresults.py
new file mode 100644
index 0000000..2f7b3e6
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/common/net/layouttestresults.py
@@ -0,0 +1,88 @@
+# Copyright (c) 2010, Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# A module for parsing results.html files generated by old-run-webkit-tests
+
+from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup, SoupStrainer
+
+
+# FIXME: This should be unified with all the layout test results code in the layout_tests package
+# This doesn't belong in common.net, but we don't have a better place for it yet.
+def path_for_layout_test(test_name):
+ return "LayoutTests/%s" % test_name
+
+
+# FIXME: This should be unified with all the layout test results code in the layout_tests package
+# This doesn't belong in common.net, but we don't have a better place for it yet.
+class LayoutTestResults(object):
+ """This class knows how to parse old-run-webkit-tests results.html files."""
+
+ stderr_key = u'Tests that had stderr output:'
+ fail_key = u'Tests where results did not match expected results:'
+ timeout_key = u'Tests that timed out:'
+ crash_key = u'Tests that caused the DumpRenderTree tool to crash:'
+ missing_key = u'Tests that had no expected results (probably new):'
+
+ expected_keys = [
+ stderr_key,
+ fail_key,
+ crash_key,
+ timeout_key,
+ missing_key,
+ ]
+
+ @classmethod
+ def _parse_results_html(cls, page):
+ parsed_results = {}
+ tables = BeautifulSoup(page).findAll("table")
+ for table in tables:
+ table_title = unicode(table.findPreviousSibling("p").string)
+ if table_title not in cls.expected_keys:
+ # This Exception should only ever be hit if run-webkit-tests changes its results.html format.
+ raise Exception("Unhandled title: %s" % table_title)
+ # We might want to translate table titles into identifiers before storing.
+ parsed_results[table_title] = [unicode(row.find("a").string) for row in table.findAll("tr")]
+
+ return parsed_results
+
+ @classmethod
+ def results_from_string(cls, string):
+ parsed_results = cls._parse_results_html(string)
+ if not parsed_results:
+ return None
+ return cls(parsed_results)
+
+ def __init__(self, parsed_results):
+ self._parsed_results = parsed_results
+
+ def parsed_results(self):
+ return self._parsed_results
+
+ def failing_tests(self):
+ failing_keys = [self.fail_key, self.crash_key, self.timeout_key]
+ return sorted(sum([tests for key, tests in self._parsed_results.items() if key in failing_keys], []))
diff --git a/WebKitTools/Scripts/webkitpy/common/net/layouttestresults_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/layouttestresults_unittest.py
new file mode 100644
index 0000000..44e4dbc
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/common/net/layouttestresults_unittest.py
@@ -0,0 +1,76 @@
+# Copyright (c) 2010, Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.common.net.layouttestresults import LayoutTestResults
+
+
+class LayoutTestResultsTest(unittest.TestCase):
+ _example_results_html = """
+<html>
+<head>
+<title>Layout Test Results</title>
+</head>
+<body>
+<p>Tests that had stderr output:</p>
+<table>
+<tr>
+<td><a href="/var/lib/buildbot/build/gtk-linux-64-release/build/LayoutTests/accessibility/aria-activedescendant-crash.html">accessibility/aria-activedescendant-crash.html</a></td>
+<td><a href="accessibility/aria-activedescendant-crash-stderr.txt">stderr</a></td>
+</tr>
+<td><a href="/var/lib/buildbot/build/gtk-linux-64-release/build/LayoutTests/http/tests/security/canvas-remote-read-svg-image.html">http/tests/security/canvas-remote-read-svg-image.html</a></td>
+<td><a href="http/tests/security/canvas-remote-read-svg-image-stderr.txt">stderr</a></td>
+</tr>
+</table><p>Tests that had no expected results (probably new):</p>
+<table>
+<tr>
+<td><a href="/var/lib/buildbot/build/gtk-linux-64-release/build/LayoutTests/fast/repaint/no-caret-repaint-in-non-content-editable-element.html">fast/repaint/no-caret-repaint-in-non-content-editable-element.html</a></td>
+<td><a href="fast/repaint/no-caret-repaint-in-non-content-editable-element-actual.txt">result</a></td>
+</tr>
+</table></body>
+</html>
+"""
+
+ _expected_layout_test_results = {
+ 'Tests that had stderr output:': [
+ 'accessibility/aria-activedescendant-crash.html',
+ ],
+ 'Tests that had no expected results (probably new):': [
+ 'fast/repaint/no-caret-repaint-in-non-content-editable-element.html',
+ ],
+ }
+
+ def test_parse_layout_test_results(self):
+ results = LayoutTestResults._parse_results_html(self._example_results_html)
+ self.assertEqual(self._expected_layout_test_results, results)
+
+ def test_results_from_string(self):
+ self.assertEqual(LayoutTestResults.results_from_string(""), None)
+ results = LayoutTestResults.results_from_string(self._example_results_html)
+ self.assertEqual(len(results.failing_tests()), 0)
diff --git a/WebKitTools/Scripts/webkitpy/common/net/networktransaction.py b/WebKitTools/Scripts/webkitpy/common/net/networktransaction.py
index c82fc6f..de19e94 100644
--- a/WebKitTools/Scripts/webkitpy/common/net/networktransaction.py
+++ b/WebKitTools/Scripts/webkitpy/common/net/networktransaction.py
@@ -29,7 +29,7 @@
import logging
import time
-from webkitpy.thirdparty.autoinstalled.mechanize import HTTPError
+from webkitpy.thirdparty.autoinstalled import mechanize
from webkitpy.common.system.deprecated_logging import log
@@ -41,10 +41,11 @@ class NetworkTimeout(Exception):
class NetworkTransaction(object):
- def __init__(self, initial_backoff_seconds=10, grown_factor=1.5, timeout_seconds=10*60):
+ def __init__(self, initial_backoff_seconds=10, grown_factor=1.5, timeout_seconds=(10 * 60), convert_404_to_None=False):
self._initial_backoff_seconds = initial_backoff_seconds
self._grown_factor = grown_factor
self._timeout_seconds = timeout_seconds
+ self._convert_404_to_None = convert_404_to_None
def run(self, request):
self._total_sleep = 0
@@ -52,7 +53,10 @@ class NetworkTransaction(object):
while True:
try:
return request()
- except HTTPError, e:
+ # FIXME: We should catch urllib2.HTTPError here too.
+ except mechanize.HTTPError, e:
+ if self._convert_404_to_None and e.code == 404:
+ return None
self._check_for_timeout()
_log.warn("Received HTTP status %s from server. Retrying in "
"%s seconds..." % (e.code, self._backoff_seconds))
diff --git a/WebKitTools/Scripts/webkitpy/common/net/networktransaction_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/networktransaction_unittest.py
index cd0702b..49aaeed 100644
--- a/WebKitTools/Scripts/webkitpy/common/net/networktransaction_unittest.py
+++ b/WebKitTools/Scripts/webkitpy/common/net/networktransaction_unittest.py
@@ -56,29 +56,36 @@ class NetworkTransactionTest(LoggingTestCase):
self.assertTrue(did_throw_exception)
self.assertTrue(did_process_exception)
- def _raise_http_error(self):
+ def _raise_500_error(self):
self._run_count += 1
if self._run_count < 3:
- raise HTTPError("http://example.com/", 500, "inteneral server error", None, None)
+ raise HTTPError("http://example.com/", 500, "internal server error", None, None)
return 42
+ def _raise_404_error(self):
+ raise HTTPError("http://foo.com/", 404, "not found", None, None)
+
def test_retry(self):
self._run_count = 0
transaction = NetworkTransaction(initial_backoff_seconds=0)
- self.assertEqual(transaction.run(lambda: self._raise_http_error()), 42)
+ self.assertEqual(transaction.run(lambda: self._raise_500_error()), 42)
self.assertEqual(self._run_count, 3)
self.assertLog(['WARNING: Received HTTP status 500 from server. '
'Retrying in 0 seconds...\n',
'WARNING: Received HTTP status 500 from server. '
'Retrying in 0.0 seconds...\n'])
+ def test_convert_404_to_None(self):
+ transaction = NetworkTransaction(convert_404_to_None=True)
+ self.assertEqual(transaction.run(lambda: self._raise_404_error()), None)
+
def test_timeout(self):
self._run_count = 0
transaction = NetworkTransaction(initial_backoff_seconds=60*60, timeout_seconds=60)
did_process_exception = False
did_throw_exception = True
try:
- transaction.run(lambda: self._raise_http_error())
+ transaction.run(lambda: self._raise_500_error())
did_throw_exception = False
except NetworkTimeout, e:
did_process_exception = True
diff --git a/WebKitTools/Scripts/webkitpy/common/net/regressionwindow.py b/WebKitTools/Scripts/webkitpy/common/net/regressionwindow.py
index 231459f..ad89815 100644
--- a/WebKitTools/Scripts/webkitpy/common/net/regressionwindow.py
+++ b/WebKitTools/Scripts/webkitpy/common/net/regressionwindow.py
@@ -28,10 +28,11 @@
class RegressionWindow(object):
- def __init__(self, build_before_failure, failing_build, common_failures=None):
+ def __init__(self, build_before_failure, failing_build, failing_tests=None):
self._build_before_failure = build_before_failure
self._failing_build = failing_build
- self._common_failures = common_failures
+ self._failing_tests = failing_tests
+ self._revisions = None
def build_before_failure(self):
return self._build_before_failure
@@ -39,10 +40,12 @@ class RegressionWindow(object):
def failing_build(self):
return self._failing_build
- def common_failures(self):
- return self._common_failures
+ def failing_tests(self):
+ return self._failing_tests
def revisions(self):
- revisions = range(self._failing_build.revision(), self._build_before_failure.revision(), -1)
- revisions.reverse()
- return revisions
+ # Cache revisions to avoid excessive allocations.
+ if not self._revisions:
+ self._revisions = range(self._failing_build.revision(), self._build_before_failure.revision(), -1)
+ self._revisions.reverse()
+ return self._revisions
diff --git a/WebKitTools/Scripts/webkitpy/common/net/statusserver.py b/WebKitTools/Scripts/webkitpy/common/net/statusserver.py
index 57390b8..3d03dcd 100644
--- a/WebKitTools/Scripts/webkitpy/common/net/statusserver.py
+++ b/WebKitTools/Scripts/webkitpy/common/net/statusserver.py
@@ -41,14 +41,18 @@ _log = logging.getLogger("webkitpy.common.net.statusserver")
class StatusServer:
default_host = "queues.webkit.org"
- def __init__(self, host=default_host):
+ def __init__(self, host=default_host, browser=None, bot_id=None):
self.set_host(host)
- self.browser = Browser()
+ self._browser = browser or Browser()
+ self.set_bot_id(bot_id)
def set_host(self, host):
self.host = host
self.url = "http://%s" % self.host
+ def set_bot_id(self, bot_id):
+ self.bot_id = bot_id
+
def results_url_for_status(self, status_id):
return "%s/results/%s" % (self.url, status_id)
@@ -56,14 +60,14 @@ class StatusServer:
if not patch:
return
if patch.bug_id():
- self.browser["bug_id"] = unicode(patch.bug_id())
+ self._browser["bug_id"] = unicode(patch.bug_id())
if patch.id():
- self.browser["patch_id"] = unicode(patch.id())
+ self._browser["patch_id"] = unicode(patch.id())
def _add_results_file(self, results_file):
if not results_file:
return
- self.browser.add_file(results_file, "text/plain", "results.txt", 'results_file')
+ self._browser.add_file(results_file, "text/plain", "results.txt", 'results_file')
def _post_status_to_server(self, queue_name, status, patch, results_file):
if results_file:
@@ -71,36 +75,61 @@ class StatusServer:
results_file.seek(0)
update_status_url = "%s/update-status" % self.url
- self.browser.open(update_status_url)
- self.browser.select_form(name="update_status")
- self.browser["queue_name"] = queue_name
+ self._browser.open(update_status_url)
+ self._browser.select_form(name="update_status")
+ self._browser["queue_name"] = queue_name
+ if self.bot_id:
+ self._browser["bot_id"] = self.bot_id
self._add_patch(patch)
- self.browser["status"] = status
+ self._browser["status"] = status
self._add_results_file(results_file)
- return self.browser.submit().read() # This is the id of the newly created status object.
+ return self._browser.submit().read() # This is the id of the newly created status object.
def _post_svn_revision_to_server(self, svn_revision_number, broken_bot):
update_svn_revision_url = "%s/update-svn-revision" % self.url
- self.browser.open(update_svn_revision_url)
- self.browser.select_form(name="update_svn_revision")
- self.browser["number"] = unicode(svn_revision_number)
- self.browser["broken_bot"] = broken_bot
- return self.browser.submit().read()
+ self._browser.open(update_svn_revision_url)
+ self._browser.select_form(name="update_svn_revision")
+ self._browser["number"] = unicode(svn_revision_number)
+ self._browser["broken_bot"] = broken_bot
+ return self._browser.submit().read()
def _post_work_items_to_server(self, queue_name, work_items):
update_work_items_url = "%s/update-work-items" % self.url
- self.browser.open(update_work_items_url)
- self.browser.select_form(name="update_work_items")
- self.browser["queue_name"] = queue_name
+ self._browser.open(update_work_items_url)
+ self._browser.select_form(name="update_work_items")
+ self._browser["queue_name"] = queue_name
work_items = map(unicode, work_items) # .join expects strings
- self.browser["work_items"] = " ".join(work_items)
- return self.browser.submit().read()
+ self._browser["work_items"] = " ".join(work_items)
+ return self._browser.submit().read()
+
+ def _post_work_item_to_ews(self, attachment_id):
+ submit_to_ews_url = "%s/submit-to-ews" % self.url
+ self._browser.open(submit_to_ews_url)
+ self._browser.select_form(name="submit_to_ews")
+ self._browser["attachment_id"] = unicode(attachment_id)
+ self._browser.submit()
+
+ def submit_to_ews(self, attachment_id):
+ _log.info("Submitting attachment %s to EWS queues" % attachment_id)
+ return NetworkTransaction().run(lambda: self._post_work_item_to_ews(attachment_id))
def next_work_item(self, queue_name):
_log.debug("Fetching next work item for %s" % queue_name)
patch_status_url = "%s/next-patch/%s" % (self.url, queue_name)
return self._fetch_url(patch_status_url)
+ def _post_release_work_item(self, queue_name, patch):
+ release_patch_url = "%s/release-patch" % (self.url)
+ self._browser.open(release_patch_url)
+ self._browser.select_form(name="release_patch")
+ self._browser["queue_name"] = queue_name
+ self._browser["attachment_id"] = unicode(patch.id())
+ self._browser.submit()
+
+ def release_work_item(self, queue_name, patch):
+ _log.debug("Releasing work item %s from %s" % (patch.id(), queue_name))
+ return NetworkTransaction(convert_404_to_None=True).run(lambda: self._post_release_work_item(queue_name, patch))
+
def update_work_items(self, queue_name, work_items):
_log.debug("Recording work items: %s for %s" % (work_items, queue_name))
return NetworkTransaction().run(lambda: self._post_work_items_to_server(queue_name, work_items))
@@ -114,6 +143,7 @@ class StatusServer:
return NetworkTransaction().run(lambda: self._post_svn_revision_to_server(svn_revision_number, broken_bot))
def _fetch_url(self, url):
+ # FIXME: This should use NetworkTransaction's 404 handling instead.
try:
return urllib2.urlopen(url).read()
except urllib2.HTTPError, e:
diff --git a/WebKitTools/Scripts/webkitpy/common/net/statusserver_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/statusserver_unittest.py
new file mode 100644
index 0000000..1169ba0
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/common/net/statusserver_unittest.py
@@ -0,0 +1,43 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.common.net.statusserver import StatusServer
+from webkitpy.common.system.outputcapture import OutputCaptureTestCaseBase
+from webkitpy.tool.mocktool import MockBrowser
+
+
+class StatusServerTest(OutputCaptureTestCaseBase):
+ def test_url_for_issue(self):
+ mock_browser = MockBrowser()
+ status_server = StatusServer(browser=mock_browser, bot_id='123')
+ status_server.update_status('queue name', 'the status')
+ self.assertEqual('queue name', mock_browser.params['queue_name'])
+ self.assertEqual('the status', mock_browser.params['status'])
+ self.assertEqual('123', mock_browser.params['bot_id'])