diff options
Diffstat (limited to 'WebKitTools/Scripts/webkitpy/common/net')
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']) |