diff options
Diffstat (limited to 'Tools/Scripts/webkitpy/tool/commands')
39 files changed, 5883 insertions, 0 deletions
diff --git a/Tools/Scripts/webkitpy/tool/commands/__init__.py b/Tools/Scripts/webkitpy/tool/commands/__init__.py new file mode 100644 index 000000000..ef05b5ac4 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/__init__.py @@ -0,0 +1,22 @@ +# Required for Python to search this directory for module files + +from webkitpy.tool.commands.adduserstogroups import AddUsersToGroups +from webkitpy.tool.commands.analyzechangelog import AnalyzeChangeLog +from webkitpy.tool.commands.applywatchlistlocal import ApplyWatchListLocal +from webkitpy.tool.commands.bugfortest import BugForTest +from webkitpy.tool.commands.bugsearch import BugSearch +from webkitpy.tool.commands.download import * +from webkitpy.tool.commands.earlywarningsystem import * +from webkitpy.tool.commands.expectations import OptimizeExpectations +from webkitpy.tool.commands.findusers import FindUsers +from webkitpy.tool.commands.gardenomatic import GardenOMatic +from webkitpy.tool.commands.openbugs import OpenBugs +from webkitpy.tool.commands.prettydiff import PrettyDiff +from webkitpy.tool.commands.queries import * +from webkitpy.tool.commands.queues import * +from webkitpy.tool.commands.rebaseline import Rebaseline +from webkitpy.tool.commands.rebaselineserver import RebaselineServer +from webkitpy.tool.commands.roll import * +from webkitpy.tool.commands.sheriffbot import * +from webkitpy.tool.commands.upload import * +from webkitpy.tool.commands.suggestnominations import * diff --git a/Tools/Scripts/webkitpy/tool/commands/abstractlocalservercommand.py b/Tools/Scripts/webkitpy/tool/commands/abstractlocalservercommand.py new file mode 100644 index 000000000..269cf25cf --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/abstractlocalservercommand.py @@ -0,0 +1,55 @@ +# Copyright (C) 2011 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: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# 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. + +from optparse import make_option +import threading + +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand + + +class AbstractLocalServerCommand(AbstractDeclarativeCommand): + server = None + launch_path = "/" + + def __init__(self): + options = [ + make_option("--httpd-port", action="store", type="int", default=8127, help="Port to use for the HTTP server"), + ] + AbstractDeclarativeCommand.__init__(self, options=options) + + def _prepare_config(self, options, args, tool): + return None + + def execute(self, options, args, tool): + config = self._prepare_config(options, args, tool) + + server_url = "http://localhost:%d%s" % (options.httpd_port, self.launch_path) + print "Starting server at %s" % server_url + print "Use the 'Exit' link in the UI, %squitquitquit or Ctrl-C to stop" % server_url + + # FIXME: This seems racy. + threading.Timer(0.1, lambda: self._tool.user.open_url(server_url)).start() + + httpd = self.server(httpd_port=options.httpd_port, config=config) + httpd.serve_forever() diff --git a/Tools/Scripts/webkitpy/tool/commands/abstractsequencedcommand.py b/Tools/Scripts/webkitpy/tool/commands/abstractsequencedcommand.py new file mode 100644 index 000000000..fd1089056 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/abstractsequencedcommand.py @@ -0,0 +1,51 @@ +# 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. + +from webkitpy.common.system.executive import ScriptError +from webkitpy.common.system.deprecated_logging import log +from webkitpy.tool.commands.stepsequence import StepSequence +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand + + +class AbstractSequencedCommand(AbstractDeclarativeCommand): + steps = None + def __init__(self): + self._sequence = StepSequence(self.steps) + AbstractDeclarativeCommand.__init__(self, self._sequence.options()) + + def _prepare_state(self, options, args, tool): + return None + + def execute(self, options, args, tool): + try: + state = self._prepare_state(options, args, tool) + except ScriptError, e: + log(e.message_with_output()) + exit(e.exit_code or 2) + + self._sequence.run_and_handle_errors(tool, options, state) diff --git a/Tools/Scripts/webkitpy/tool/commands/adduserstogroups.py b/Tools/Scripts/webkitpy/tool/commands/adduserstogroups.py new file mode 100644 index 000000000..22869584d --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/adduserstogroups.py @@ -0,0 +1,65 @@ +# Copyright (c) 2011 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. + +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand + + +class AddUsersToGroups(AbstractDeclarativeCommand): + name = "add-users-to-groups" + help_text = "Add users matching subtring to specified groups" + + # This probably belongs in bugzilla.py + known_groups = ['canconfirm', 'editbugs'] + + def execute(self, options, args, tool): + search_string = args[0] + # FIXME: We could allow users to specify groups on the command line. + list_title = 'Add users matching "%s" which groups?' % search_string + # FIXME: Need a way to specify that "none" is not allowed. + # FIXME: We could lookup what groups the current user is able to grant from bugzilla. + groups = tool.user.prompt_with_list(list_title, self.known_groups, can_choose_multiple=True) + if not groups: + print "No groups specified." + return + + login_userid_pairs = tool.bugs.queries.fetch_login_userid_pairs_matching_substring(search_string) + if not login_userid_pairs: + print "No users found matching '%s'" % search_string + return + + print "Found %s users matching %s:" % (len(login_userid_pairs), search_string) + for (login, user_id) in login_userid_pairs: + print "%s (%s)" % (login, user_id) + + confirm_message = "Are you sure you want add %s users to groups %s? (This action cannot be undone using webkit-patch.)" % (len(login_userid_pairs), groups) + if not tool.user.confirm(confirm_message): + return + + for (login, user_id) in login_userid_pairs: + print "Adding %s to %s" % (login, groups) + tool.bugs.add_user_to_groups(user_id, groups) diff --git a/Tools/Scripts/webkitpy/tool/commands/analyzechangelog.py b/Tools/Scripts/webkitpy/tool/commands/analyzechangelog.py new file mode 100644 index 000000000..2fe34ade5 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/analyzechangelog.py @@ -0,0 +1,208 @@ +# Copyright (c) 2011 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. + +from __future__ import with_statement + +import json +import re +import time + +from webkitpy.common.checkout.scm.detection import SCMDetector +from webkitpy.common.checkout.changelog import ChangeLog +from webkitpy.common.config.contributionareas import ContributionAreas +from webkitpy.common.system.filesystem import FileSystem +from webkitpy.common.system.executive import Executive +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand +from webkitpy.tool import steps + + +class AnalyzeChangeLog(AbstractDeclarativeCommand): + name = "analyze-changelog" + help_text = "Experimental command for analyzing change logs." + long_help = "This command parses changelogs in a specified directory and summarizes the result as JSON files." + + def __init__(self): + options = [ + steps.Options.changelog_count, + ] + AbstractDeclarativeCommand.__init__(self, options=options) + + @staticmethod + def _enumerate_changelogs(filesystem, dirname, changelog_count): + changelogs = [filesystem.join(dirname, filename) for filename in filesystem.listdir(dirname) if re.match('^ChangeLog(-(\d{4}-\d{2}-\d{2}))?$', filename)] + # Make sure ChangeLog shows up before ChangeLog-2011-01-01 + changelogs = sorted(changelogs, key=lambda filename: filename + 'X', reverse=True) + return changelogs[:changelog_count] + + @staticmethod + def _generate_jsons(filesystem, jsons, output_dir): + for filename in jsons: + print ' Generating', filename + filesystem.write_text_file(filesystem.join(output_dir, filename), json.dumps(jsons[filename], indent=2)) + + def execute(self, options, args, tool): + filesystem = self._tool.filesystem + if len(args) < 1 or not filesystem.exists(args[0]): + print "Need the directory name to look for changelog as the first argument" + return + changelog_dir = filesystem.abspath(args[0]) + + if len(args) < 2 or not filesystem.exists(args[1]): + print "Need the output directory name as the second argument" + return + output_dir = args[1] + + startTime = time.time() + + print 'Enumerating ChangeLog files...' + changelogs = AnalyzeChangeLog._enumerate_changelogs(filesystem, changelog_dir, options.changelog_count) + + analyzer = ChangeLogAnalyzer(tool, changelogs) + analyzer.analyze() + + print 'Generating json files...' + json_files = { + 'summary.json': analyzer.summary(), + 'contributors.json': analyzer.contributors_statistics(), + 'areas.json': analyzer.areas_statistics(), + } + AnalyzeChangeLog._generate_jsons(filesystem, json_files, output_dir) + commands_dir = filesystem.dirname(filesystem.path_to_module(self.__module__)) + print commands_dir + filesystem.copyfile(filesystem.join(commands_dir, 'data/summary.html'), filesystem.join(output_dir, 'summary.html')) + + tick = time.time() - startTime + print 'Finished in %02dm:%02ds' % (int(tick / 60), int(tick % 60)) + + +class ChangeLogAnalyzer(object): + def __init__(self, host, changelog_paths): + self._changelog_paths = changelog_paths + self._filesystem = host.filesystem + self._contribution_areas = ContributionAreas(host.filesystem) + self._scm = host.scm() + self._parsed_revisions = {} + + self._contributors_statistics = {} + self._areas_statistics = dict([(area, {'reviewed': 0, 'unreviewed': 0, 'contributors': {}}) for area in self._contribution_areas.names()]) + self._summary = {'reviewed': 0, 'unreviewed': 0} + + self._longest_filename = max([len(path) - len(self._scm.checkout_root) for path in changelog_paths]) + self._filename = '' + self._length_of_previous_output = 0 + + def contributors_statistics(self): + return self._contributors_statistics + + def areas_statistics(self): + return self._areas_statistics + + def summary(self): + return self._summary + + def _print_status(self, status): + if self._length_of_previous_output: + print "\r" + " " * self._length_of_previous_output, + new_output = ('%' + str(self._longest_filename) + 's: %s') % (self._filename, status) + print "\r" + new_output, + self._length_of_previous_output = len(new_output) + + def _set_filename(self, filename): + if self._filename: + print + self._filename = filename + + def analyze(self): + for path in self._changelog_paths: + self._set_filename(self._filesystem.relpath(path, self._scm.checkout_root)) + with self._filesystem.open_text_file_for_reading(path) as changelog: + self._print_status('Parsing entries...') + number_of_parsed_entries = self._analyze_entries(ChangeLog.parse_entries_from_file(changelog), path) + self._print_status('Done (%d entries)' % number_of_parsed_entries) + print + self._summary['contributors'] = len(self._contributors_statistics) + self._summary['contributors_with_reviews'] = sum([1 for contributor in self._contributors_statistics.values() if contributor['reviews']['total']]) + self._summary['contributors_without_reviews'] = self._summary['contributors'] - self._summary['contributors_with_reviews'] + + def _collect_statistics_for_contributor_area(self, area, contributor, contribution_type, reviewed): + area_contributors = self._areas_statistics[area]['contributors'] + if contributor not in area_contributors: + area_contributors[contributor] = {'reviews': 0, 'reviewed': 0, 'unreviewed': 0} + if contribution_type == 'patches': + contribution_type = 'reviewed' if reviewed else 'unreviewed' + area_contributors[contributor][contribution_type] += 1 + + def _collect_statistics_for_contributor(self, contributor, contribution_type, areas, touched_files, reviewed): + if contributor not in self._contributors_statistics: + self._contributors_statistics[contributor] = { + 'reviews': {'total': 0, 'areas': {}, 'files': {}}, + 'patches': {'reviewed': 0, 'unreviewed': 0, 'areas': {}, 'files': {}}} + statistics = self._contributors_statistics[contributor][contribution_type] + + if contribution_type == 'reviews': + statistics['total'] += 1 + elif reviewed: + statistics['reviewed'] += 1 + else: + statistics['unreviewed'] += 1 + + for area in areas: + self._increment_dictionary_value(statistics['areas'], area) + self._collect_statistics_for_contributor_area(area, contributor, contribution_type, reviewed) + for touchedfile in touched_files: + self._increment_dictionary_value(statistics['files'], touchedfile) + + def _increment_dictionary_value(self, dictionary, key): + dictionary[key] = dictionary.get(key, 0) + 1 + + def _analyze_entries(self, entries, changelog_path): + dirname = self._filesystem.dirname(changelog_path) + for i, entry in enumerate(entries): + self._print_status('(%s) entries' % i) + assert(entry.authors()) + + touchedfiles_for_entry = [self._filesystem.relpath(self._filesystem.join(dirname, name), self._scm.checkout_root) for name in entry.touched_files()] + areas_for_entry = self._contribution_areas.areas_for_touched_files(touchedfiles_for_entry) + authors_for_entry = entry.authors() + reviewers_for_entry = entry.reviewers() + + for reviewer in reviewers_for_entry: + self._collect_statistics_for_contributor(reviewer.full_name, 'reviews', areas_for_entry, touchedfiles_for_entry, reviewed=True) + + for author in authors_for_entry: + self._collect_statistics_for_contributor(author['name'], 'patches', areas_for_entry, touchedfiles_for_entry, + reviewed=bool(reviewers_for_entry)) + + for area in areas_for_entry: + self._areas_statistics[area]['reviewed' if reviewers_for_entry else 'unreviewed'] += 1 + + self._summary['reviewed' if reviewers_for_entry else 'unreviewed'] += 1 + + i += 1 + self._print_status('(%s) entries' % i) + return i diff --git a/Tools/Scripts/webkitpy/tool/commands/analyzechangelog_unittest.py b/Tools/Scripts/webkitpy/tool/commands/analyzechangelog_unittest.py new file mode 100644 index 000000000..661d2d85f --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/analyzechangelog_unittest.py @@ -0,0 +1,185 @@ +# Cpyright (c) 2011 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 json +import sys +from webkitpy.common.config.contributionareas import ContributionAreas +from webkitpy.common.host_mock import MockHost +from webkitpy.common.system.filesystem_mock import MockFileSystem +from webkitpy.common.system.executive import Executive +from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.tool.commands.analyzechangelog import AnalyzeChangeLog +from webkitpy.tool.commands.analyzechangelog import ChangeLogAnalyzer +from webkitpy.tool.commands.commandtest import CommandsTest +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand + + +class AnalyzeChangeLogTest(CommandsTest): + def test_enumerate_enumerate_changelogs(self): + filesystem = MockFileSystem({ + 'foo/ChangeLog': '', + 'foo/ChangeLog-2010-06-23': '', + 'foo/ChangeLog-2010-12-31': '', + 'foo/ChangeLog-x': '', + 'foo/ChangeLog-2011-01-01': '', + }) + changelogs = AnalyzeChangeLog._enumerate_changelogs(filesystem, 'foo/', None) + self.assertEqual(changelogs, ['foo/ChangeLog', 'foo/ChangeLog-2011-01-01', 'foo/ChangeLog-2010-12-31', 'foo/ChangeLog-2010-06-23']) + + changelogs = AnalyzeChangeLog._enumerate_changelogs(filesystem, 'foo/', 2) + self.assertEqual(changelogs, ['foo/ChangeLog', 'foo/ChangeLog-2011-01-01']) + + def test_generate_jsons(self): + filesystem = MockFileSystem() + test_json = {'array.json': [1, 2, 3, {'key': 'value'}], 'dictionary.json': {'somekey': 'somevalue', 'array': [4, 5]}} + + capture = OutputCapture() + capture.capture_output() + + AnalyzeChangeLog._generate_jsons(filesystem, test_json, 'bar') + self.assertEqual(set(filesystem.files.keys()), set(['bar/array.json', 'bar/dictionary.json'])) + + capture.restore_output() + + self.assertEqual(json.loads(filesystem.files['bar/array.json']), test_json['array.json']) + self.assertEqual(json.loads(filesystem.files['bar/dictionary.json']), test_json['dictionary.json']) + + +class ChangeLogAnalyzerTest(CommandsTest): + def test_analyze_one_changelog(self): + host = MockHost() + host.filesystem.files['mock-checkout/foo/ChangeLog'] = u"""2011-11-17 Mark Rowe <mrowe@apple.com> + + <http://webkit.org/b/72646> Disable deprecation warnings around code where we cannot easily + switch away from the deprecated APIs. + + Reviewed by Sam Weinig. + + * platform/mac/WebCoreNSStringExtras.mm: + * platform/network/cf/SocketStreamHandleCFNet.cpp: + (WebCore::SocketStreamHandle::reportErrorToClient): + +2011-11-19 Kevin Ollivier <kevino@theolliviers.com> + + [wx] C++ bindings build fix for move of array classes to WTF. + + * bindings/scripts/CodeGeneratorCPP.pm: + (GetCPPTypeGetter): + (GetNamespaceForClass): + (GenerateHeader): + (GenerateImplementation): + +2011-10-27 Philippe Normand <pnormand@igalia.com> and Zan Dobersek <zandobersek@gmail.com> + + [GStreamer] WebAudio AudioFileReader implementation + https://bugs.webkit.org/show_bug.cgi?id=69834 + + Reviewed by Martin Robinson. + + Basic FileReader implementation, supporting one or 2 audio + channels. An empty AudioDestination is also provided, its complete + implementation is handled in bug 69835. + + * GNUmakefile.am: + * GNUmakefile.list.am: + * platform/audio/gstreamer/AudioDestinationGStreamer.cpp: Added. + (WebCore::AudioDestination::create): + (WebCore::AudioDestination::hardwareSampleRate): + (WebCore::AudioDestinationGStreamer::AudioDestinationGStreamer): + (WebCore::AudioDestinationGStreamer::~AudioDestinationGStreamer): + (WebCore::AudioDestinationGStreamer::start): + (WebCore::AudioDestinationGStreamer::stop): + * platform/audio/gstreamer/AudioDestinationGStreamer.h: Added. + (WebCore::AudioDestinationGStreamer::isPlaying): + (WebCore::AudioDestinationGStreamer::sampleRate): + (WebCore::AudioDestinationGStreamer::sourceProvider): + * platform/audio/gstreamer/AudioFileReaderGStreamer.cpp: Added. + (WebCore::getGStreamerAudioCaps): + (WebCore::getFloatFromByteReader): + (WebCore::copyGstreamerBuffersToAudioChannel): + (WebCore::onAppsinkNewBufferCallback): + (WebCore::messageCallback): + (WebCore::onGStreamerDeinterleavePadAddedCallback): + (WebCore::onGStreamerDeinterleaveReadyCallback): + (WebCore::onGStreamerDecodebinPadAddedCallback): + (WebCore::AudioFileReader::AudioFileReader): + (WebCore::AudioFileReader::~AudioFileReader): + (WebCore::AudioFileReader::handleBuffer): + (WebCore::AudioFileReader::handleMessage): + (WebCore::AudioFileReader::handleNewDeinterleavePad): + (WebCore::AudioFileReader::deinterleavePadsConfigured): + (WebCore::AudioFileReader::plugDeinterleave): + (WebCore::AudioFileReader::createBus): + (WebCore::createBusFromAudioFile): + (WebCore::createBusFromInMemoryAudioFile): + * platform/audio/gtk/AudioBusGtk.cpp: Added. + (WebCore::AudioBus::loadPlatformResource): +""" + + capture = OutputCapture() + capture.capture_output() + + analyzer = ChangeLogAnalyzer(host, ['mock-checkout/foo/ChangeLog']) + analyzer.analyze() + + capture.restore_output() + + self.assertEqual(analyzer.summary(), + {'reviewed': 2, 'unreviewed': 1, 'contributors': 6, 'contributors_with_reviews': 2, 'contributors_without_reviews': 4}) + + self.assertEqual(set(analyzer.contributors_statistics().keys()), + set(['Sam Weinig', u'Mark Rowe', u'Kevin Ollivier', 'Martin Robinson', u'Philippe Normand', u'Zan Dobersek'])) + + self.assertEqual(analyzer.contributors_statistics()['Sam Weinig'], + {'reviews': {'files': {u'foo/platform/mac/WebCoreNSStringExtras.mm': 1, u'foo/platform/network/cf/SocketStreamHandleCFNet.cpp': 1}, + 'total': 1, 'areas': {'Network': 1}}, 'patches': {'files': {}, 'areas': {}, 'unreviewed': 0, 'reviewed': 0}}) + self.assertEqual(analyzer.contributors_statistics()[u'Mark Rowe'], + {'reviews': {'files': {}, 'total': 0, 'areas': {}}, 'patches': {'files': {u'foo/platform/mac/WebCoreNSStringExtras.mm': 1, + u'foo/platform/network/cf/SocketStreamHandleCFNet.cpp': 1}, 'areas': {'Network': 1}, 'unreviewed': 0, 'reviewed': 1}}) + self.assertEqual(analyzer.contributors_statistics()[u'Kevin Ollivier'], + {'reviews': {'files': {}, 'total': 0, 'areas': {}}, 'patches': {'files': {u'foo/bindings/scripts/CodeGeneratorCPP.pm': 1}, + 'areas': {'Bindings': 1}, 'unreviewed': 1, 'reviewed': 0}}) + + files_for_audio_patch = {u'foo/GNUmakefile.am': 1, u'foo/GNUmakefile.list.am': 1, 'foo/platform/audio/gstreamer/AudioDestinationGStreamer.cpp': 1, + 'foo/platform/audio/gstreamer/AudioDestinationGStreamer.h': 1, 'foo/platform/audio/gstreamer/AudioFileReaderGStreamer.cpp': 1, + 'foo/platform/audio/gtk/AudioBusGtk.cpp': 1} + author_expectation_for_audio_patch = {'reviews': {'files': {}, 'total': 0, 'areas': {}}, + 'patches': {'files': files_for_audio_patch, 'areas': {'The WebKitGTK+ Port': 1}, 'unreviewed': 0, 'reviewed': 1}} + self.assertEqual(analyzer.contributors_statistics()[u'Martin Robinson'], + {'reviews': {'files': files_for_audio_patch, 'total': 1, 'areas': {'The WebKitGTK+ Port': 1}}, + 'patches': {'files': {}, 'areas': {}, 'unreviewed': 0, 'reviewed': 0}}) + self.assertEqual(analyzer.contributors_statistics()[u'Philippe Normand'], author_expectation_for_audio_patch) + self.assertEqual(analyzer.contributors_statistics()[u'Zan Dobersek'], author_expectation_for_audio_patch) + + areas_statistics = analyzer.areas_statistics() + areas_with_patches = [area for area in areas_statistics if areas_statistics[area]['reviewed'] or areas_statistics[area]['unreviewed']] + self.assertEqual(set(areas_with_patches), set(['Bindings', 'Network', 'The WebKitGTK+ Port'])) + self.assertEqual(areas_statistics['Bindings'], {'unreviewed': 1, 'reviewed': 0, 'contributors': + {u'Kevin Ollivier': {'reviews': 0, 'unreviewed': 1, 'reviewed': 0}}}) + self.assertEqual(areas_statistics['Network'], {'unreviewed': 0, 'reviewed': 1, 'contributors': + {'Sam Weinig': {'reviews': 1, 'unreviewed': 0, 'reviewed': 0}, u'Mark Rowe': {'reviews': 0, 'unreviewed': 0, 'reviewed': 1}}}) diff --git a/Tools/Scripts/webkitpy/tool/commands/applywatchlistlocal.py b/Tools/Scripts/webkitpy/tool/commands/applywatchlistlocal.py new file mode 100644 index 000000000..6735d4842 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/applywatchlistlocal.py @@ -0,0 +1,50 @@ +# Copyright (c) 2011 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. + +from webkitpy.tool.commands.abstractsequencedcommand import AbstractSequencedCommand +from webkitpy.tool import steps + + +class ApplyWatchListLocal(AbstractSequencedCommand): + name = "apply-watchlist-local" + help_text = "Applies the watchlist to local changes" + argument_names = "[BUGID]" + steps = [ + steps.ApplyWatchList, + ] + long_help = """"Applies the watchlist to local changes. +The results is logged if a bug is no given. This may be used to try out a watchlist change.""" + + def _prepare_state(self, options, args, tool): + if len(args) > 1: + raise Exception("Too many arguments given: %s" % (' '.join(args))) + if not args: + return {} + return { + "bug_id": args[0], + } diff --git a/Tools/Scripts/webkitpy/tool/commands/applywatchlistlocal_unittest.py b/Tools/Scripts/webkitpy/tool/commands/applywatchlistlocal_unittest.py new file mode 100644 index 000000000..91818d1c2 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/applywatchlistlocal_unittest.py @@ -0,0 +1,50 @@ +# Copyright (c) 2011 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. + + +from webkitpy.tool.commands.commandtest import CommandsTest +from webkitpy.tool.commands.applywatchlistlocal import ApplyWatchListLocal + + +class ApplyWatchListLocalTest(CommandsTest): + def test_args_parsing(self): + expected_stderr = 'MockWatchList: determine_cc_and_messages\n' + self.assert_execute_outputs(ApplyWatchListLocal(), [''], expected_stderr=expected_stderr) + + def test_args_parsing_with_bug(self): + expected_stderr = """MockWatchList: determine_cc_and_messages +MOCK bug comment: bug_id=50002, cc=set(['eric@webkit.org', 'levin@chromium.org', 'abarth@webkit.org']) +--- Begin comment --- +Message1. + +Message2. +--- End comment ---\n\n""" + self.assert_execute_outputs(ApplyWatchListLocal(), ['50002'], expected_stderr=expected_stderr) + + def test_args_parsing_with_two_bugs(self): + self._assertRaisesRegexp(Exception, 'Too many arguments given: 1234 5678', self.assert_execute_outputs, ApplyWatchListLocal(), ['1234', '5678']) diff --git a/Tools/Scripts/webkitpy/tool/commands/bugfortest.py b/Tools/Scripts/webkitpy/tool/commands/bugfortest.py new file mode 100644 index 000000000..36aa6b5f1 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/bugfortest.py @@ -0,0 +1,48 @@ +# 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. + +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand +from webkitpy.tool.bot.flakytestreporter import FlakyTestReporter + + +# This is mostly a command for testing FlakyTestReporter, however +# it could be easily expanded to auto-create bugs, etc. if another +# command outside of webkitpy wanted to use it. +class BugForTest(AbstractDeclarativeCommand): + name = "bug-for-test" + help_text = "Finds the bugzilla bug for a given test" + + def execute(self, options, args, tool): + reporter = FlakyTestReporter(tool, "webkitpy") + search_string = args[0] + bug = reporter._lookup_bug_for_flaky_test(search_string) + if bug: + bug = reporter._follow_duplicate_chain(bug) + print "%5s %s" % (bug.id(), bug.title()) + else: + print "No bugs found matching '%s'" % search_string diff --git a/Tools/Scripts/webkitpy/tool/commands/bugsearch.py b/Tools/Scripts/webkitpy/tool/commands/bugsearch.py new file mode 100644 index 000000000..5cbc1a044 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/bugsearch.py @@ -0,0 +1,42 @@ +# 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. + +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand + + +class BugSearch(AbstractDeclarativeCommand): + name = "bug-search" + help_text = "List bugs matching a query" + + def execute(self, options, args, tool): + search_string = args[0] + bugs = tool.bugs.queries.fetch_bugs_matching_quicksearch(search_string) + for bug in bugs: + print "%5s %s" % (bug.id(), bug.title()) + if not bugs: + print "No bugs found matching '%s'" % search_string diff --git a/Tools/Scripts/webkitpy/tool/commands/commandtest.py b/Tools/Scripts/webkitpy/tool/commands/commandtest.py new file mode 100644 index 000000000..eea0a6156 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/commandtest.py @@ -0,0 +1,48 @@ +# Copyright (C) 2009 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. + +from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.common.webkitunittest import TestCase +from webkitpy.tool.mocktool import MockOptions, MockTool + + +class CommandsTest(TestCase): + def assert_execute_outputs(self, command, args=[], expected_stdout="", expected_stderr="", expected_exception=None, options=MockOptions(), tool=MockTool()): + options.blocks = None + options.cc = 'MOCK cc' + options.component = 'MOCK component' + options.confirm = True + options.email = 'MOCK email' + options.git_commit = 'MOCK git commit' + options.obsolete_patches = True + options.open_bug = True + options.port = 'MOCK port' + options.quiet = True + options.reviewer = 'MOCK reviewer' + command.bind_to_tool(tool) + OutputCapture().assert_outputs(self, command.execute, [options, args, tool], expected_stdout=expected_stdout, expected_stderr=expected_stderr, expected_exception=expected_exception) diff --git a/Tools/Scripts/webkitpy/tool/commands/data/summary.html b/Tools/Scripts/webkitpy/tool/commands/data/summary.html new file mode 100644 index 000000000..abf80d84f --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/data/summary.html @@ -0,0 +1,455 @@ +<!DOCTYPE html> +<html> +<head> +<title>ChangeLog Analysis</title> +<style type="text/css"> + +body { + font-family: 'Helvetica' 'Segoe UI Light' sans-serif; + font-weight: 200; + padding: 20px; + min-width: 1200px; +} + +* { + padding: 0px; + margin: 0px; + border: 0px; +} + +h1, h2, h3 { + font-weight: 200; +} + +h1 { + margin: 0 0 1em 0; +} + +h2 { + font-size: 1.2em; + text-align: center; + margin-bottom: 1em; +} + +h3 { + font-size: 1em; +} + +.view { + margin: 0px; + width: 600px; + float: left; +} + +.graph-container p { + width: 200px; + text-align: right; + margin: 20px 0 20px 0; + padding: 5px; + border-right: solid 1px black; +} + +.graph-container table { + width: 100%; +} + +.graph-container table, .graph-container td { + border-collapse: collapse; + border: none; +} + +.graph-container td { + padding: 5px; + vertical-align: center; +} + +.graph-container td:first-child { + width: 200px; + text-align: right; + border-right: solid 1px black; +} + +.graph-container .selected { + background: #eee; +} + +#reviewers .selected td:first-child { + border-radius: 10px 0px 0px 10px; +} + +#areas .selected td:last-child { + border-radius: 0px 10px 10px 0px; +} + +.graph-container .bar { + display: inline-block; + min-height: 1em; + background: #9f6; + margin-right: 0.4ex; +} + +.graph-container .reviewed-patches { + background: #3cf; + margin-right: 1px; +} + +.graph-container .unreviewed-patches { + background: #f99; +} + +.constrained { + background: #eee; + border-radius: 10px; +} + +.constrained .vertical-bar { + border-right: solid 1px #eee; +} + +#header { + border-spacing: 5px; +} + +#header section { + display: table-cell; + width: 200px; + vertical-align: top; + border: solid 2px #ccc; + border-collapse: collapse; + padding: 5px; + font-size: 0.8em; +} + +#header dt { + float: left; +} + +#header dt:after { + content: ': '; +} + +#header .legend { + width: 600px; +} + +.legend .bar { + width: 15ex; + padding: 2px; +} + +.legend .reviews { + width: 25ex; +} + +.legend td:first-child { + width: 18ex; +} + +</style> +</head> +<body> +<h1>ChangeLog Analysis</h1> + +<section id="header"> +<section id="summary"> +<h2>Summary</h2> +</section> + +<section class="legend"> +<h2>Legend</h2> +<div class="graph-container"> +<table> +<tbody> +<tr><td>Contributor's name</td> +<td><span class="bar reviews">Reviews</span> <span class="value-container">(# of reviews)</span><br> +<span class="bar reviewed-patches">Reviewed</span><span class="bar unreviewed-patches">Unreviewed</span> +<span class="value-container">(# of reviewed):(# of unreviewed)</span></td></tr> +</tbody> +</table> +</div> +</section> +</section> + +<section id="contributors" class="view"> +<h2 id="contributors-title">Contributors</h2> +<div class="graph-container"></div> +</section> + +<section id="areas" class="view"> +<h2 id="areas-title">Areas of contributions</h2> +<div class="graph-container"></div> +</section> + +<script> + +// Naive implementation of element extensions discussed on public-webapps + +if (!Element.prototype.append) { + Element.prototype.append = function () { + for (var i = 0; i < arguments.length; i++) { + // FIXME: Take care of other node types + if (arguments[i] instanceof Element || arguments[i] instanceof CharacterData) + this.appendChild(arguments[i]); + else + this.appendChild(document.createTextNode(arguments[i])); + } + return this; + } +} + +if (!Node.prototype.remove) { + Node.prototype.remove = function () { + this.parentNode.removeChild(this); + return this; + } +} + +if (!Element.create) { + Element.create = function () { + if (arguments.length < 1) + return null; + var element = document.createElement(arguments[0]); + if (arguments.length == 1) + return element; + + // FIXME: the second argument can be content or IDL attributes + var attributes = arguments[1]; + for (attribute in attributes) + element.setAttribute(attribute, attributes[attribute]); + + if (arguments.length >= 3) + element.append.apply(element, arguments[2]); + + return element; + } +} + +if (!Node.prototype.removeAllChildren) { + Node.prototype.removeAllChildren = function () { + while (this.firstChild) + this.firstChild.remove(); + return this; + } +} + +Element.prototype.removeClassNameFromAllElements = function (className) { + var elements = this.getElementsByClassName(className); + for (var i = 0; i < elements.length; i++) + elements[i].classList.remove(className); +} + +function getJSON(url, callback) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.onreadystatechange = function () { + if (this.readyState == 4) + callback(JSON.parse(xhr.responseText)); + } + xhr.send(); +} + +function GraphView(container) { + this._container = container; + this._defaultData = null; +} + +GraphView.prototype.setData = function(data, constrained) { + if (constrained) + this._container.classList.add('constrained'); + else + this._container.classList.remove('constrained'); + this._clearGraph(); + this._constructGraph(data); +} + +GraphView.prototype.setDefaultData = function(data) { + this._defaultData = data; + this.setData(data); +} + +GraphView.prototype.reset = function () { + this.setMarginTop(); + this.setData(this._defaultData); +} + +GraphView.prototype.isConstrained = function () { return this._container.classList.contains('constrained'); } + +GraphView.prototype.targetRow = function (node) { + var target = null; + + while (node && node != this._container) { + if (node.localName == 'tr') + target = node; + node = node.parentNode; + } + + return node && target; +} + +GraphView.prototype.selectRow = function (row) { + this._container.removeClassNameFromAllElements('selected'); + row.classList.add('selected'); +} + +GraphView.prototype.setMarginTop = function (y) { this._container.style.marginTop = y ? y + 'px' : null; } +GraphView.prototype._graphContainer = function () { return this._container.getElementsByClassName('graph-container')[0]; } +GraphView.prototype._clearGraph = function () { return this._graphContainer().removeAllChildren(); } + +GraphView.prototype._numberOfPatches = function (dataItem) { + return dataItem.numberOfReviewedPatches + (dataItem.numberOfUnreviewedPatches !== undefined ? dataItem.numberOfUnreviewedPatches : 0); +} + +GraphView.prototype._maximumValue = function (labels, data) { + var numberOfPatches = this._numberOfPatches; + return Math.max.apply(null, labels.map(function (label) { + return Math.max(numberOfPatches(data[label]), data[label].numberOfReviews !== undefined ? data[label].numberOfReviews : 0); + })); +} + +GraphView.prototype._sortLabelsByNumberOfReviwsAndReviewedPatches = function(data) { + var labels = Object.keys(data); + if (!labels.length) + return null; + var numberOfPatches = this._numberOfPatches; + var computeValue = function (dataItem) { + return numberOfPatches(dataItem) + (dataItem.numberOfReviews !== undefined ? dataItem.numberOfReviews : 0); + } + labels.sort(function (a, b) { return computeValue(data[b]) - computeValue(data[a]); }); + return labels; +} + +GraphView.prototype._constructGraph = function (data) { + var element = this._graphContainer(); + var labels = this._sortLabelsByNumberOfReviwsAndReviewedPatches(data); + if (!labels) { + element.append(Element.create('p', {}, ['None'])); + return; + } + + var maxValue = this._maximumValue(labels, data); + var computeStyleForBar = function (value) { return 'width:' + (value * 85.0 / maxValue) + '%' } + + var table = Element.create('table', {}, [Element.create('tbody')]); + for (var i = 0; i < labels.length; i++) { + var label = labels[i]; + var item = data[label]; + var row = Element.create('tr', {}, [Element.create('td', {}, [label]), Element.create('td', {})]); + var valueCell = row.lastChild; + + if (item.numberOfReviews != undefined) { + valueCell.append( + Element.create('span', {'class': 'bar reviews', 'style': computeStyleForBar(item.numberOfReviews) }), + Element.create('span', {'class': 'value-container'}, [item.numberOfReviews]), + Element.create('br') + ); + } + + valueCell.append(Element.create('span', {'class': 'bar reviewed-patches', 'style': computeStyleForBar(item.numberOfReviewedPatches) })); + if (item.numberOfUnreviewedPatches !== undefined) + valueCell.append(Element.create('span', {'class': 'bar unreviewed-patches', 'style': computeStyleForBar(item.numberOfUnreviewedPatches) })); + + valueCell.append(Element.create('span', {'class': 'value-container'}, + [item.numberOfReviewedPatches + (item.numberOfUnreviewedPatches !== undefined ? ':' + item.numberOfUnreviewedPatches : '')])); + + table.firstChild.append(row); + row.label = label; + row.data = item; + } + element.append(table); +} + +var contributorsView = new GraphView(document.querySelector('#contributors')); +var areasView = new GraphView(document.querySelector('#areas')); + +getJSON('summary.json', + function (summary) { + var summaryContainer = document.querySelector('#summary'); + summaryContainer.append(Element.create('dl', {}, [ + Element.create('dt', {}, ['Total entries (reviewed)']), + Element.create('dd', {}, [(summary['reviewed'] + summary['unreviewed']) + ' (' + summary['reviewed'] + ')']), + Element.create('dt', {}, ['Total contributors']), + Element.create('dd', {}, [summary['contributors']]), + Element.create('dt', {}, ['Contributors who reviewed']), + Element.create('dd', {}, [summary['contributors_with_reviews']]), + ])); + }); + +getJSON('contributors.json', + function (contributors) { + for (var contributor in contributors) { + contributor = contributors[contributor]; + contributor.numberOfReviews = contributor.reviews ? contributor.reviews.total : 0; + contributor.numberOfReviewedPatches = contributor.patches ? contributor.patches.reviewed : 0; + contributor.numberOfUnreviewedPatches = contributor.patches ? contributor.patches.unreviewed : 0; + } + contributorsView.setDefaultData(contributors); + }); + +getJSON('areas.json', + function (areas) { + for (var area in areas) { + areas[area].numberOfReviewedPatches = areas[area].reviewed; + areas[area].numberOfUnreviewedPatches = areas[area].unreviewed; + } + areasView.setDefaultData(areas); + }); + +function contributorAreas(contributorData) { + var areas = new Object; + for (var area in contributorData.reviews.areas) { + if (!areas[area]) + areas[area] = {'numberOfReviewedPatches': 0}; + areas[area].numberOfReviews = contributorData.reviews.areas[area]; + } + for (var area in contributorData.patches.areas) { + if (!areas[area]) + areas[area] = {'numberOfReviews': 0}; + areas[area].numberOfReviewedPatches = contributorData.patches.areas[area]; + } + return areas; +} + +function areaContributors(areaData) { + var contributors = areaData['contributors']; + for (var contributor in contributors) { + contributor = contributors[contributor]; + contributor.numberOfReviews = contributor.reviews; + contributor.numberOfReviewedPatches = contributor.reviewed; + contributor.numberOfUnreviewedPatches = contributor.unreviewed; + } + return contributors; +} + +var mouseTimer = 0; +window.onmouseover = function (event) { + clearTimeout(mouseTimer); + + var row = contributorsView.targetRow(event.target); + if (row) { + if (!contributorsView.isConstrained()) { + contributorsView.selectRow(row); + areasView.setMarginTop(row.firstChild.offsetTop); + areasView.setData(contributorAreas(row.data), 'constrained'); + } + return; + } + + row = areasView.targetRow(event.target); + if (row) { + if (!areasView.isConstrained()) { + areasView.selectRow(row); + contributorsView.setMarginTop(row.firstChild.offsetTop); + contributorsView.setData(areaContributors(row.data), 'constrained'); + } + return; + } + + mouseTimer = setTimeout(function () { + contributorsView.reset(); + areasView.reset(); + }, 500); +} + +</script> +</body> +</html> diff --git a/Tools/Scripts/webkitpy/tool/commands/download.py b/Tools/Scripts/webkitpy/tool/commands/download.py new file mode 100644 index 000000000..2ba4986e2 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/download.py @@ -0,0 +1,434 @@ +# Copyright (c) 2009, 2011 Google Inc. All rights reserved. +# Copyright (c) 2009 Apple 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 os + +from webkitpy.tool import steps + +from webkitpy.common.checkout.changelog import ChangeLog +from webkitpy.common.config import urls +from webkitpy.common.system.executive import ScriptError +from webkitpy.tool.commands.abstractsequencedcommand import AbstractSequencedCommand +from webkitpy.tool.commands.stepsequence import StepSequence +from webkitpy.tool.comments import bug_comment_from_commit_text +from webkitpy.tool.grammar import pluralize +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand +from webkitpy.common.system.deprecated_logging import error, log + + +class Clean(AbstractSequencedCommand): + name = "clean" + help_text = "Clean the working copy" + steps = [ + steps.CleanWorkingDirectory, + ] + + def _prepare_state(self, options, args, tool): + options.force_clean = True + + +class Update(AbstractSequencedCommand): + name = "update" + help_text = "Update working copy (used internally)" + steps = [ + steps.CleanWorkingDirectory, + steps.Update, + ] + + +class Build(AbstractSequencedCommand): + name = "build" + help_text = "Update working copy and build" + steps = [ + steps.CleanWorkingDirectory, + steps.Update, + steps.Build, + ] + + def _prepare_state(self, options, args, tool): + options.build = True + + +class BuildAndTest(AbstractSequencedCommand): + name = "build-and-test" + help_text = "Update working copy, build, and run the tests" + steps = [ + steps.CleanWorkingDirectory, + steps.Update, + steps.Build, + steps.RunTests, + ] + + +class Land(AbstractSequencedCommand): + name = "land" + help_text = "Land the current working directory diff and updates the associated bug if any" + argument_names = "[BUGID]" + show_in_main_help = True + steps = [ + steps.UpdateChangeLogsWithReviewer, + steps.ValidateReviewer, + steps.ValidateChangeLogs, # We do this after UpdateChangeLogsWithReviewer to avoid not having to cache the diff twice. + steps.Build, + steps.RunTests, + steps.Commit, + steps.CloseBugForLandDiff, + ] + long_help = """land commits the current working copy diff (just as svn or git commit would). +land will NOT build and run the tests before committing, but you can use the --build option for that. +If a bug id is provided, or one can be found in the ChangeLog land will update the bug after committing.""" + + def _prepare_state(self, options, args, tool): + changed_files = self._tool.scm().changed_files(options.git_commit) + return { + "changed_files": changed_files, + "bug_id": (args and args[0]) or tool.checkout().bug_id_for_this_commit(options.git_commit, changed_files), + } + + +class LandCowboy(AbstractSequencedCommand): + name = "land-cowboy" + help_text = "Prepares a ChangeLog and lands the current working directory diff." + steps = [ + steps.PrepareChangeLog, + steps.EditChangeLog, + steps.CheckStyle, + steps.ConfirmDiff, + steps.Build, + steps.RunTests, + steps.Commit, + steps.CloseBugForLandDiff, + ] + + def _prepare_state(self, options, args, tool): + options.check_style_filter = "-changelog" + + +class AbstractPatchProcessingCommand(AbstractDeclarativeCommand): + # Subclasses must implement the methods below. We don't declare them here + # because we want to be able to implement them with mix-ins. + # + # def _fetch_list_of_patches_to_process(self, options, args, tool): + # def _prepare_to_process(self, options, args, tool): + + @staticmethod + def _collect_patches_by_bug(patches): + bugs_to_patches = {} + for patch in patches: + bugs_to_patches[patch.bug_id()] = bugs_to_patches.get(patch.bug_id(), []) + [patch] + return bugs_to_patches + + def execute(self, options, args, tool): + self._prepare_to_process(options, args, tool) + patches = self._fetch_list_of_patches_to_process(options, args, tool) + + # It's nice to print out total statistics. + bugs_to_patches = self._collect_patches_by_bug(patches) + log("Processing %s from %s." % (pluralize("patch", len(patches)), pluralize("bug", len(bugs_to_patches)))) + + for patch in patches: + self._process_patch(patch, options, args, tool) + + +class AbstractPatchSequencingCommand(AbstractPatchProcessingCommand): + prepare_steps = None + main_steps = None + + def __init__(self): + options = [] + self._prepare_sequence = StepSequence(self.prepare_steps) + self._main_sequence = StepSequence(self.main_steps) + options = sorted(set(self._prepare_sequence.options() + self._main_sequence.options())) + AbstractPatchProcessingCommand.__init__(self, options) + + def _prepare_to_process(self, options, args, tool): + self._prepare_sequence.run_and_handle_errors(tool, options) + + def _process_patch(self, patch, options, args, tool): + state = { "patch" : patch } + self._main_sequence.run_and_handle_errors(tool, options, state) + + +class ProcessAttachmentsMixin(object): + def _fetch_list_of_patches_to_process(self, options, args, tool): + return map(lambda patch_id: tool.bugs.fetch_attachment(patch_id), args) + + +class ProcessBugsMixin(object): + def _fetch_list_of_patches_to_process(self, options, args, tool): + all_patches = [] + for bug_id in args: + patches = tool.bugs.fetch_bug(bug_id).reviewed_patches() + log("%s found on bug %s." % (pluralize("reviewed patch", len(patches)), bug_id)) + all_patches += patches + return all_patches + + +class CheckStyle(AbstractPatchSequencingCommand, ProcessAttachmentsMixin): + name = "check-style" + help_text = "Run check-webkit-style on the specified attachments" + argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]" + main_steps = [ + steps.CleanWorkingDirectory, + steps.Update, + steps.ApplyPatch, + steps.CheckStyle, + ] + + +class BuildAttachment(AbstractPatchSequencingCommand, ProcessAttachmentsMixin): + name = "build-attachment" + help_text = "Apply and build patches from bugzilla" + argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]" + main_steps = [ + steps.CleanWorkingDirectory, + steps.Update, + steps.ApplyPatch, + steps.Build, + ] + + +class BuildAndTestAttachment(AbstractPatchSequencingCommand, ProcessAttachmentsMixin): + name = "build-and-test-attachment" + help_text = "Apply, build, and test patches from bugzilla" + argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]" + main_steps = [ + steps.CleanWorkingDirectory, + steps.Update, + steps.ApplyPatch, + steps.Build, + steps.RunTests, + ] + + +class AbstractPatchApplyingCommand(AbstractPatchSequencingCommand): + prepare_steps = [ + steps.EnsureLocalCommitIfNeeded, + steps.CleanWorkingDirectoryWithLocalCommits, + steps.Update, + ] + main_steps = [ + steps.ApplyPatchWithLocalCommit, + ] + long_help = """Updates the working copy. +Downloads and applies the patches, creating local commits if necessary.""" + + +class ApplyAttachment(AbstractPatchApplyingCommand, ProcessAttachmentsMixin): + name = "apply-attachment" + help_text = "Apply an attachment to the local working directory" + argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]" + show_in_main_help = True + + +class ApplyFromBug(AbstractPatchApplyingCommand, ProcessBugsMixin): + name = "apply-from-bug" + help_text = "Apply reviewed patches from provided bugs to the local working directory" + argument_names = "BUGID [BUGIDS]" + show_in_main_help = True + + +class ApplyWatchList(AbstractPatchSequencingCommand, ProcessAttachmentsMixin): + name = "apply-watchlist" + help_text = "Applies the watchlist to the specified attachments" + argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]" + main_steps = [ + steps.CleanWorkingDirectory, + steps.Update, + steps.ApplyPatch, + steps.ApplyWatchList, + ] + long_help = """"Applies the watchlist to the specified attachments. +Downloads the attachment, applies it locally, runs the watchlist against it, and updates the bug with the result.""" + + +class AbstractPatchLandingCommand(AbstractPatchSequencingCommand): + main_steps = [ + steps.CleanWorkingDirectory, + steps.Update, + steps.ApplyPatch, + steps.ValidateChangeLogs, + steps.ValidateReviewer, + steps.Build, + steps.RunTests, + steps.Commit, + steps.ClosePatch, + steps.CloseBug, + ] + long_help = """Checks to make sure builders are green. +Updates the working copy. +Applies the patch. +Builds. +Runs the layout tests. +Commits the patch. +Clears the flags on the patch. +Closes the bug if no patches are marked for review.""" + + +class LandAttachment(AbstractPatchLandingCommand, ProcessAttachmentsMixin): + name = "land-attachment" + help_text = "Land patches from bugzilla, optionally building and testing them first" + argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]" + show_in_main_help = True + + +class LandFromBug(AbstractPatchLandingCommand, ProcessBugsMixin): + name = "land-from-bug" + help_text = "Land all patches on the given bugs, optionally building and testing them first" + argument_names = "BUGID [BUGIDS]" + show_in_main_help = True + + +class ValidateChangelog(AbstractSequencedCommand): + name = "validate-changelog" + help_text = "Validate that the ChangeLogs and reviewers look reasonable" + long_help = """Examines the current diff to see whether the ChangeLogs +and the reviewers listed in the ChangeLogs look reasonable. +""" + steps = [ + steps.ValidateChangeLogs, + steps.ValidateReviewer, + ] + + +class AbstractRolloutPrepCommand(AbstractSequencedCommand): + argument_names = "REVISION [REVISIONS] REASON" + + def _commit_info(self, revision): + commit_info = self._tool.checkout().commit_info_for_revision(revision) + if commit_info and commit_info.bug_id(): + # Note: Don't print a bug URL here because it will confuse the + # SheriffBot because the SheriffBot just greps the output + # of create-rollout for bug URLs. It should do better + # parsing instead. + log("Preparing rollout for bug %s." % commit_info.bug_id()) + else: + log("Unable to parse bug number from diff.") + return commit_info + + def _prepare_state(self, options, args, tool): + revision_list = [] + for revision in str(args[0]).split(): + if revision.isdigit(): + revision_list.append(int(revision)) + else: + raise ScriptError(message="Invalid svn revision number: " + revision) + revision_list.sort() + + # We use the earliest revision for the bug info + earliest_revision = revision_list[0] + commit_info = self._commit_info(earliest_revision) + cc_list = sorted([party.bugzilla_email() + for party in commit_info.responsible_parties() + if party.bugzilla_email()]) + return { + "revision": earliest_revision, + "revision_list": revision_list, + "bug_id": commit_info.bug_id(), + # FIXME: We should used the list as the canonical representation. + "bug_cc": ",".join(cc_list), + "reason": args[1], + } + + +class PrepareRollout(AbstractRolloutPrepCommand): + name = "prepare-rollout" + help_text = "Revert the given revision(s) in the working copy and prepare ChangeLogs with revert reason" + long_help = """Updates the working copy. +Applies the inverse diff for the provided revision(s). +Creates an appropriate rollout ChangeLog, including a trac link and bug link. +""" + steps = [ + steps.CleanWorkingDirectory, + steps.Update, + steps.RevertRevision, + steps.PrepareChangeLogForRevert, + ] + + +class CreateRollout(AbstractRolloutPrepCommand): + name = "create-rollout" + help_text = "Creates a bug to track the broken SVN revision(s) and uploads a rollout patch." + steps = [ + steps.CleanWorkingDirectory, + steps.Update, + steps.RevertRevision, + steps.CreateBug, + steps.PrepareChangeLogForRevert, + steps.PostDiffForRevert, + ] + + def _prepare_state(self, options, args, tool): + state = AbstractRolloutPrepCommand._prepare_state(self, options, args, tool) + # Currently, state["bug_id"] points to the bug that caused the + # regression. We want to create a new bug that blocks the old bug + # so we move state["bug_id"] to state["bug_blocked"] and delete the + # old state["bug_id"] so that steps.CreateBug will actually create + # the new bug that we want (and subsequently store its bug id into + # state["bug_id"]) + state["bug_blocked"] = state["bug_id"] + del state["bug_id"] + state["bug_title"] = "REGRESSION(r%s): %s" % (state["revision"], state["reason"]) + state["bug_description"] = "%s broke the build:\n%s" % (urls.view_revision_url(state["revision"]), state["reason"]) + # FIXME: If we had more context here, we could link to other open bugs + # that mention the test that regressed. + if options.parent_command == "sheriff-bot": + state["bug_description"] += """ + +This is an automatic bug report generated by the sheriff-bot. If this bug +report was created because of a flaky test, please file a bug for the flaky +test (if we don't already have one on file) and dup this bug against that bug +so that we can track how often these flaky tests case pain. + +"Only you can prevent forest fires." -- Smokey the Bear +""" + return state + + +class Rollout(AbstractRolloutPrepCommand): + name = "rollout" + show_in_main_help = True + help_text = "Revert the given revision(s) in the working copy and optionally commit the revert and re-open the original bug" + long_help = """Updates the working copy. +Applies the inverse diff for the provided revision. +Creates an appropriate rollout ChangeLog, including a trac link and bug link. +Opens the generated ChangeLogs in $EDITOR. +Shows the prepared diff for confirmation. +Commits the revert and updates the bug (including re-opening the bug if necessary).""" + steps = [ + steps.CleanWorkingDirectory, + steps.Update, + steps.RevertRevision, + steps.PrepareChangeLogForRevert, + steps.EditChangeLog, + steps.ConfirmDiff, + steps.Build, + steps.Commit, + steps.ReopenBugAfterRollout, + ] diff --git a/Tools/Scripts/webkitpy/tool/commands/download_unittest.py b/Tools/Scripts/webkitpy/tool/commands/download_unittest.py new file mode 100644 index 000000000..8b63dfcb6 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/download_unittest.py @@ -0,0 +1,252 @@ +# Copyright (C) 2009, 2011 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.system.outputcapture import OutputCapture +from webkitpy.thirdparty.mock import Mock +from webkitpy.tool.commands.commandtest import CommandsTest +from webkitpy.tool.commands.download import * +from webkitpy.tool.mocktool import MockOptions, MockTool +from webkitpy.common.checkout.checkout_mock import MockCheckout + + +class AbstractRolloutPrepCommandTest(unittest.TestCase): + def test_commit_info(self): + command = AbstractRolloutPrepCommand() + tool = MockTool() + command.bind_to_tool(tool) + output = OutputCapture() + + expected_stderr = "Preparing rollout for bug 50000.\n" + commit_info = output.assert_outputs(self, command._commit_info, [1234], expected_stderr=expected_stderr) + self.assertTrue(commit_info) + + mock_commit_info = Mock() + mock_commit_info.bug_id = lambda: None + tool._checkout.commit_info_for_revision = lambda revision: mock_commit_info + expected_stderr = "Unable to parse bug number from diff.\n" + commit_info = output.assert_outputs(self, command._commit_info, [1234], expected_stderr=expected_stderr) + self.assertEqual(commit_info, mock_commit_info) + + def test_prepare_state(self): + command = AbstractRolloutPrepCommand() + mock_commit_info = MockCheckout().commit_info_for_revision(123) + command._commit_info = lambda revision: mock_commit_info + + state = command._prepare_state(None, ["124 123 125", "Reason"], None) + self.assertEqual(123, state["revision"]) + self.assertEqual([123, 124, 125], state["revision_list"]) + + self.assertRaises(ScriptError, command._prepare_state, options=None, args=["125 r122 123", "Reason"], tool=None) + self.assertRaises(ScriptError, command._prepare_state, options=None, args=["125 foo 123", "Reason"], tool=None) + + +class DownloadCommandsTest(CommandsTest): + def _default_options(self): + options = MockOptions() + options.build = True + options.build_style = True + options.check_style = True + options.check_style_filter = None + options.clean = True + options.close_bug = True + options.force_clean = False + options.force_patch = True + options.non_interactive = False + options.parent_command = 'MOCK parent command' + options.quiet = False + options.test = True + options.update = True + return options + + def test_build(self): + expected_stderr = "Updating working directory\nBuilding WebKit\n" + self.assert_execute_outputs(Build(), [], options=self._default_options(), expected_stderr=expected_stderr) + + def test_build_and_test(self): + expected_stderr = "Updating working directory\nBuilding WebKit\nRunning Python unit tests\nRunning Perl unit tests\nRunning JavaScriptCore tests\nRunning run-webkit-tests\n" + self.assert_execute_outputs(BuildAndTest(), [], options=self._default_options(), expected_stderr=expected_stderr) + + def test_apply_attachment(self): + options = self._default_options() + options.update = True + options.local_commit = True + expected_stderr = "Updating working directory\nProcessing 1 patch from 1 bug.\nProcessing patch 10000 from bug 50000.\n" + self.assert_execute_outputs(ApplyAttachment(), [10000], options=options, expected_stderr=expected_stderr) + + def test_apply_patches(self): + options = self._default_options() + options.update = True + options.local_commit = True + expected_stderr = "Updating working directory\n2 reviewed patches found on bug 50000.\nProcessing 2 patches from 1 bug.\nProcessing patch 10000 from bug 50000.\nProcessing patch 10001 from bug 50000.\n" + self.assert_execute_outputs(ApplyFromBug(), [50000], options=options, expected_stderr=expected_stderr) + + def test_apply_watch_list(self): + expected_stderr = """Processing 1 patch from 1 bug. +Updating working directory +MOCK run_and_throw_if_fail: ['mock-update-webkit'], cwd=/mock-checkout\nProcessing patch 10000 from bug 50000. +MockWatchList: determine_cc_and_messages +""" + self.assert_execute_outputs(ApplyWatchList(), [10000], options=self._default_options(), expected_stderr=expected_stderr, tool=MockTool(log_executive=True)) + + def test_land(self): + expected_stderr = "Building WebKit\nRunning Python unit tests\nRunning Perl unit tests\nRunning JavaScriptCore tests\nRunning run-webkit-tests\nCommitted r49824: <http://trac.webkit.org/changeset/49824>\nUpdating bug 50000\n" + mock_tool = MockTool() + mock_tool.scm().create_patch = Mock(return_value="Patch1\nMockPatch\n") + mock_tool.checkout().modified_changelogs = Mock(return_value=[]) + self.assert_execute_outputs(Land(), [50000], options=self._default_options(), expected_stderr=expected_stderr, tool=mock_tool) + # Make sure we're not calling expensive calls too often. + self.assertEqual(mock_tool.scm().create_patch.call_count, 0) + self.assertEqual(mock_tool.checkout().modified_changelogs.call_count, 1) + + def test_land_cowboy(self): + expected_stderr = """MOCK run_and_throw_if_fail: ['mock-prepare-ChangeLog', '--email=MOCK email', '--merge-base=None', 'MockFile1'], cwd=/mock-checkout +MOCK run_and_throw_if_fail: ['mock-check-webkit-style', '--git-commit', 'MOCK git commit', '--diff-files', 'MockFile1', '--filter', '-changelog'], cwd=/mock-checkout +MOCK run_command: ['ruby', '-I', '/mock-checkout/Websites/bugs.webkit.org/PrettyPatch', '/mock-checkout/Websites/bugs.webkit.org/PrettyPatch/prettify.rb'], cwd=None +MOCK: user.open_url: file://... +Was that diff correct? +Building WebKit +MOCK run_and_throw_if_fail: ['mock-build-webkit'], cwd=/mock-checkout, env={'LC_ALL': 'C', 'MOCK_ENVIRON_COPY': '1'} +Running Python unit tests +MOCK run_and_throw_if_fail: ['mock-test-webkitpy'], cwd=/mock-checkout +Running Perl unit tests +MOCK run_and_throw_if_fail: ['mock-test-webkitperl'], cwd=/mock-checkout +Running JavaScriptCore tests +MOCK run_and_throw_if_fail: ['mock-run-javacriptcore-tests'], cwd=/mock-checkout +Running run-webkit-tests +MOCK run_and_throw_if_fail: ['mock-run-webkit-tests', '--quiet'], cwd=/mock-checkout +Committed r49824: <http://trac.webkit.org/changeset/49824> +Committed r49824: <http://trac.webkit.org/changeset/49824> +No bug id provided. +""" + mock_tool = MockTool(log_executive=True) + self.assert_execute_outputs(LandCowboy(), [50000], options=self._default_options(), expected_stderr=expected_stderr, tool=mock_tool) + + def test_land_red_builders(self): + expected_stderr = 'Building WebKit\nRunning Python unit tests\nRunning Perl unit tests\nRunning JavaScriptCore tests\nRunning run-webkit-tests\nCommitted r49824: <http://trac.webkit.org/changeset/49824>\nUpdating bug 50000\n' + mock_tool = MockTool() + mock_tool.buildbot.light_tree_on_fire() + self.assert_execute_outputs(Land(), [50000], options=self._default_options(), expected_stderr=expected_stderr, tool=mock_tool) + + def test_check_style(self): + expected_stderr = """Processing 1 patch from 1 bug. +Updating working directory +MOCK run_and_throw_if_fail: ['mock-update-webkit'], cwd=/mock-checkout +Processing patch 10000 from bug 50000. +MOCK run_and_throw_if_fail: ['mock-check-webkit-style', '--git-commit', 'MOCK git commit', '--diff-files', 'MockFile1'], cwd=/mock-checkout +""" + self.assert_execute_outputs(CheckStyle(), [10000], options=self._default_options(), expected_stderr=expected_stderr, tool=MockTool(log_executive=True)) + + def test_build_attachment(self): + expected_stderr = "Processing 1 patch from 1 bug.\nUpdating working directory\nProcessing patch 10000 from bug 50000.\nBuilding WebKit\n" + self.assert_execute_outputs(BuildAttachment(), [10000], options=self._default_options(), expected_stderr=expected_stderr) + + def test_land_attachment(self): + # FIXME: This expected result is imperfect, notice how it's seeing the same patch as still there after it thought it would have cleared the flags. + expected_stderr = """Processing 1 patch from 1 bug. +Updating working directory +Processing patch 10000 from bug 50000. +Building WebKit +Running Python unit tests +Running Perl unit tests +Running JavaScriptCore tests +Running run-webkit-tests +Committed r49824: <http://trac.webkit.org/changeset/49824> +Not closing bug 50000 as attachment 10000 has review=+. Assuming there are more patches to land from this bug. +""" + self.assert_execute_outputs(LandAttachment(), [10000], options=self._default_options(), expected_stderr=expected_stderr) + + def test_land_patches(self): + # FIXME: This expected result is imperfect, notice how it's seeing the same patch as still there after it thought it would have cleared the flags. + expected_stderr = """2 reviewed patches found on bug 50000. +Processing 2 patches from 1 bug. +Updating working directory +Processing patch 10000 from bug 50000. +Building WebKit +Running Python unit tests +Running Perl unit tests +Running JavaScriptCore tests +Running run-webkit-tests +Committed r49824: <http://trac.webkit.org/changeset/49824> +Not closing bug 50000 as attachment 10000 has review=+. Assuming there are more patches to land from this bug. +Updating working directory +Processing patch 10001 from bug 50000. +Building WebKit +Running Python unit tests +Running Perl unit tests +Running JavaScriptCore tests +Running run-webkit-tests +Committed r49824: <http://trac.webkit.org/changeset/49824> +Not closing bug 50000 as attachment 10000 has review=+. Assuming there are more patches to land from this bug. +""" + self.assert_execute_outputs(LandFromBug(), [50000], options=self._default_options(), expected_stderr=expected_stderr) + + def test_prepare_rollout(self): + expected_stderr = "Preparing rollout for bug 50000.\nUpdating working directory\n" + self.assert_execute_outputs(PrepareRollout(), [852, "Reason"], options=self._default_options(), expected_stderr=expected_stderr) + + def test_create_rollout(self): + expected_stderr = """Preparing rollout for bug 50000. +Updating working directory +MOCK create_bug +bug_title: REGRESSION(r852): Reason +bug_description: http://trac.webkit.org/changeset/852 broke the build: +Reason +component: MOCK component +cc: MOCK cc +blocked: 50000 +MOCK add_patch_to_bug: bug_id=50004, description=ROLLOUT of r852, mark_for_review=False, mark_for_commit_queue=True, mark_for_landing=False +-- Begin comment -- +Any committer can land this patch automatically by marking it commit-queue+. The commit-queue will build and test the patch before landing to ensure that the rollout will be successful. This process takes approximately 15 minutes. + +If you would like to land the rollout faster, you can use the following command: + + webkit-patch land-attachment ATTACHMENT_ID + +where ATTACHMENT_ID is the ID of this attachment. +-- End comment -- +""" + self.assert_execute_outputs(CreateRollout(), [852, "Reason"], options=self._default_options(), expected_stderr=expected_stderr) + self.assert_execute_outputs(CreateRollout(), ["855 852 854", "Reason"], options=self._default_options(), expected_stderr=expected_stderr) + + def test_rollout(self): + expected_stderr = """Preparing rollout for bug 50000. +Updating working directory +MOCK: user.open_url: file://... +Was that diff correct? +Building WebKit +Committed r49824: <http://trac.webkit.org/changeset/49824> +MOCK reopen_bug 50000 with comment 'Reverted r852 for reason: + +Reason + +Committed r49824: <http://trac.webkit.org/changeset/49824>' +""" + self.assert_execute_outputs(Rollout(), [852, "Reason"], options=self._default_options(), expected_stderr=expected_stderr) + diff --git a/Tools/Scripts/webkitpy/tool/commands/earlywarningsystem.py b/Tools/Scripts/webkitpy/tool/commands/earlywarningsystem.py new file mode 100644 index 000000000..338149b6d --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/earlywarningsystem.py @@ -0,0 +1,209 @@ +# Copyright (c) 2009 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. + +from webkitpy.common.config.committers import CommitterList +from webkitpy.common.config.ports import WebKitPort +from webkitpy.common.system.deprecated_logging import error, log +from webkitpy.common.system.executive import ScriptError +from webkitpy.tool.bot.expectedfailures import ExpectedFailures +from webkitpy.tool.bot.layouttestresultsreader import LayoutTestResultsReader +from webkitpy.tool.bot.queueengine import QueueEngine +from webkitpy.tool.bot.earlywarningsystemtask import EarlyWarningSystemTask, EarlyWarningSystemTaskDelegate, UnableToApplyPatch +from webkitpy.tool.commands.queues import AbstractReviewQueue + + +class AbstractEarlyWarningSystem(AbstractReviewQueue, EarlyWarningSystemTaskDelegate): + _build_style = "release" + # FIXME: Switch _run_tests from opt-in to opt-out once more bots are ready to run tests. + _run_tests = False + + def __init__(self): + AbstractReviewQueue.__init__(self) + self.port = WebKitPort.port(self.port_name) + + def should_proceed_with_work_item(self, patch): + return True + + def begin_work_queue(self): + # FIXME: This violates abstraction + self._tool._deprecated_port = self.port + AbstractReviewQueue.begin_work_queue(self) + self._expected_failures = ExpectedFailures() + self._layout_test_results_reader = LayoutTestResultsReader(self._tool, self._log_directory()) + + def _failing_tests_message(self, task, patch): + results = task.results_from_patch_test_run(patch) + unexpected_failures = self._expected_failures.unexpected_failures_observed(results) + if not unexpected_failures: + return None + return "New failing tests:\n%s" % "\n".join(unexpected_failures) + + def _post_reject_message_on_bug(self, tool, patch, status_id, extra_message_text=None): + results_link = tool.status_server.results_url_for_status(status_id) + message = "Attachment %s did not pass %s (%s):\nOutput: %s" % (patch.id(), self.name, self.port_name, results_link) + # FIXME: We might want to add some text about rejecting from the commit-queue in + # the case where patch.commit_queue() isn't already set to '-'. + if self.watchers: + tool.bugs.add_cc_to_bug(patch.bug_id(), self.watchers) + tool.bugs.set_flag_on_attachment(patch.id(), "commit-queue", "-", message, extra_message_text) + + def review_patch(self, patch): + task = EarlyWarningSystemTask(self, patch, self._run_tests) + if not task.validate(): + self._did_error(patch, "%s did not process patch." % self.name) + return False + try: + return task.run() + except UnableToApplyPatch, e: + self._did_error(patch, "%s unable to apply patch." % self.name) + return False + except ScriptError, e: + self._post_reject_message_on_bug(self._tool, patch, task.failure_status_id, self._failing_tests_message(task, patch)) + results_archive = task.results_archive_from_patch_test_run(patch) + if results_archive: + self._upload_results_archive_for_patch(patch, results_archive) + self._did_fail(patch) + # FIXME: We're supposed to be able to raise e again here and have + # one of our base classes mark the patch as fail, but there seems + # to be an issue with the exit_code. + return False + + # EarlyWarningSystemDelegate methods + + def parent_command(self): + return self.name + + def run_command(self, command): + self.run_webkit_patch(command + [self.port.flag()]) + + def command_passed(self, message, patch): + pass + + def command_failed(self, message, script_error, patch): + failure_log = self._log_from_script_error_for_upload(script_error) + return self._update_status(message, patch=patch, results_file=failure_log) + + def expected_failures(self): + return self._expected_failures + + def layout_test_results(self): + return self._layout_test_results_reader.results() + + def archive_last_layout_test_results(self, patch): + return self._layout_test_results_reader.archive(patch) + + def build_style(self): + return self._build_style + + def refetch_patch(self, patch): + return self._tool.bugs.fetch_attachment(patch.id()) + + def report_flaky_tests(self, patch, flaky_test_results, results_archive): + pass + + # StepSequenceErrorHandler methods + + @classmethod + def handle_script_error(cls, tool, state, script_error): + # FIXME: Why does this not exit(1) like the superclass does? + log(script_error.message_with_output()) + + +class GtkEWS(AbstractEarlyWarningSystem): + name = "gtk-ews" + port_name = "gtk" + watchers = AbstractEarlyWarningSystem.watchers + [ + "gns@gnome.org", + "xan.lopez@gmail.com", + ] + + +class EflEWS(AbstractEarlyWarningSystem): + name = "efl-ews" + port_name = "efl" + watchers = AbstractEarlyWarningSystem.watchers + [ + "leandro@profusion.mobi", + "antognolli@profusion.mobi", + "lucas.demarchi@profusion.mobi", + "gyuyoung.kim@samsung.com", + ] + + +class QtEWS(AbstractEarlyWarningSystem): + name = "qt-ews" + port_name = "qt" + + +class WinEWS(AbstractEarlyWarningSystem): + name = "win-ews" + port_name = "win" + # Use debug, the Apple Win port fails to link Release on 32-bit Windows. + # https://bugs.webkit.org/show_bug.cgi?id=39197 + _build_style = "debug" + + +class AbstractChromiumEWS(AbstractEarlyWarningSystem): + port_name = "chromium" + watchers = AbstractEarlyWarningSystem.watchers + [ + "dglazkov@chromium.org", + ] + + +class ChromiumLinuxEWS(AbstractChromiumEWS): + # FIXME: We should rename this command to cr-linux-ews, but that requires + # a database migration. :( + name = "chromium-ews" + port_name = "chromium-xvfb" + _run_tests = True + + +class ChromiumWindowsEWS(AbstractChromiumEWS): + name = "cr-win-ews" + + +# For platforms that we can't run inside a VM (like Mac OS X), we require +# patches to be uploaded by committers, who are generally trustworthy folk. :) +class AbstractCommitterOnlyEWS(AbstractEarlyWarningSystem): + def process_work_item(self, patch): + if not patch.attacher() or not patch.attacher().can_commit: + self._did_error(patch, "%s cannot process patches from non-committers :(" % self.name) + return False + return AbstractEarlyWarningSystem.process_work_item(self, patch) + + +# FIXME: Inheriting from AbstractCommitterOnlyEWS is kinda a hack, but it +# happens to work because AbstractChromiumEWS and AbstractCommitterOnlyEWS +# provide disjoint sets of functionality, and Python is otherwise smart +# enough to handle the diamond inheritance. +class ChromiumMacEWS(AbstractChromiumEWS, AbstractCommitterOnlyEWS): + name = "cr-mac-ews" + + +class MacEWS(AbstractCommitterOnlyEWS): + name = "mac-ews" + port_name = "mac" diff --git a/Tools/Scripts/webkitpy/tool/commands/earlywarningsystem_unittest.py b/Tools/Scripts/webkitpy/tool/commands/earlywarningsystem_unittest.py new file mode 100644 index 000000000..44143f72d --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/earlywarningsystem_unittest.py @@ -0,0 +1,99 @@ +# Copyright (C) 2009 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 os + +from webkitpy.thirdparty.mock import Mock +from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.tool.bot.queueengine import QueueEngine +from webkitpy.tool.commands.earlywarningsystem import * +from webkitpy.tool.commands.queuestest import QueuesTest +from webkitpy.tool.mocktool import MockTool, MockOptions + + +class AbstractEarlyWarningSystemTest(QueuesTest): + def test_failing_tests_message(self): + # Needed to define port_name, used in AbstractEarlyWarningSystem.__init__ + class TestEWS(AbstractEarlyWarningSystem): + port_name = "win" # Needs to be a port which port/factory understands. + + ews = TestEWS() + ews.bind_to_tool(MockTool()) + ews._options = MockOptions(port=None, confirm=False) + OutputCapture().assert_outputs(self, ews.begin_work_queue, expected_stderr=self._default_begin_work_queue_stderr(ews.name)) + ews._expected_failures.unexpected_failures_observed = lambda results: set(["foo.html", "bar.html"]) + task = Mock() + patch = ews._tool.bugs.fetch_attachment(10000) + self.assertEqual(ews._failing_tests_message(task, patch), "New failing tests:\nbar.html\nfoo.html") + + +class EarlyWarningSytemTest(QueuesTest): + def _default_expected_stderr(self, ews): + string_replacemnts = { + "name": ews.name, + "port": ews.port_name, + } + expected_stderr = { + "begin_work_queue": self._default_begin_work_queue_stderr(ews.name), + "handle_unexpected_error": "Mock error message\n", + "next_work_item": "", + "process_work_item": "MOCK: update_status: %(name)s Pass\nMOCK: release_work_item: %(name)s 10000\n" % string_replacemnts, + "handle_script_error": "ScriptError error message\n", + } + return expected_stderr + + def _test_builder_ews(self, ews): + ews.bind_to_tool(MockTool()) + self.assert_queue_outputs(ews, expected_stderr=self._default_expected_stderr(ews)) + + def _test_committer_only_ews(self, ews): + ews.bind_to_tool(MockTool()) + expected_stderr = self._default_expected_stderr(ews) + string_replacemnts = {"name": ews.name} + expected_stderr["process_work_item"] = "MOCK: update_status: %(name)s Error: %(name)s cannot process patches from non-committers :(\nMOCK: release_work_item: %(name)s 10000\n" % string_replacemnts + self.assert_queue_outputs(ews, expected_stderr=expected_stderr) + + def _test_testing_ews(self, ews): + ews.layout_test_results = lambda: None + ews.bind_to_tool(MockTool()) + expected_stderr = self._default_expected_stderr(ews) + expected_stderr["handle_script_error"] = "ScriptError error message\n" + self.assert_queue_outputs(ews, expected_stderr=expected_stderr) + + def test_committer_only_ewses(self): + self._test_committer_only_ews(MacEWS()) + self._test_committer_only_ews(ChromiumMacEWS()) + + def test_builder_ewses(self): + self._test_builder_ews(ChromiumWindowsEWS()) + self._test_builder_ews(QtEWS()) + self._test_builder_ews(GtkEWS()) + self._test_builder_ews(EflEWS()) + + def test_testing_ewses(self): + self._test_testing_ews(ChromiumLinuxEWS()) diff --git a/Tools/Scripts/webkitpy/tool/commands/expectations.py b/Tools/Scripts/webkitpy/tool/commands/expectations.py new file mode 100644 index 000000000..575e80ce8 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/expectations.py @@ -0,0 +1,45 @@ +# Copyright (c) 2011 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. + +from webkitpy.layout_tests.models.test_configuration import TestConfigurationConverter +from webkitpy.layout_tests.models.test_expectations import TestExpectationParser, TestExpectationSerializer +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand + + +class OptimizeExpectations(AbstractDeclarativeCommand): + name = "optimize-expectations" + help_text = "Fixes simple style issues in test_expectations file. (Currently works only for chromium port.)" + + def execute(self, options, args, tool): + port = tool.port_factory.get("chromium-win-win7") # FIXME: This should be selectable. + expectation_lines = TestExpectationParser.tokenize_list(port.test_expectations()) + parser = TestExpectationParser(port, [], allow_rebaseline_modifier=False) + for expectation_line in expectation_lines: + parser.parse(expectation_line) + converter = TestConfigurationConverter(port.all_test_configurations(), port.configuration_specifier_macros()) + tool.filesystem.write_text_file(port.path_to_test_expectations_file(), TestExpectationSerializer.list_to_string(expectation_lines, converter)) diff --git a/Tools/Scripts/webkitpy/tool/commands/findusers.py b/Tools/Scripts/webkitpy/tool/commands/findusers.py new file mode 100644 index 000000000..4363c8cf2 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/findusers.py @@ -0,0 +1,44 @@ +# Copyright (c) 2011 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. + +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand + + +class FindUsers(AbstractDeclarativeCommand): + name = "find-users" + help_text = "Find users matching substring" + + def execute(self, options, args, tool): + search_string = args[0] + login_userid_pairs = tool.bugs.queries.fetch_login_userid_pairs_matching_substring(search_string) + for (login, user_id) in login_userid_pairs: + user = tool.bugs.fetch_user(user_id) + groups_string = ", ".join(user['groups']) if user['groups'] else "none" + print "%s <%s> (%s) (%s)" % (user['name'], user['login'], user_id, groups_string) + else: + print "No users found matching '%s'" % search_string diff --git a/Tools/Scripts/webkitpy/tool/commands/gardenomatic.py b/Tools/Scripts/webkitpy/tool/commands/gardenomatic.py new file mode 100644 index 000000000..7da96e4bc --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/gardenomatic.py @@ -0,0 +1,41 @@ +# Copyright (C) 2011 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: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# 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. + +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand +from webkitpy.tool.servers.gardeningserver import GardeningHTTPServer + + +class GardenOMatic(AbstractDeclarativeCommand): + name = "garden-o-matic" + help_text = "Experimental command for gardening the WebKit tree." + + def execute(self, options, args, tool): + print "This command runs a local HTTP server that changes your working copy" + print "based on the actions you take in the web-based UI." + + httpd = GardeningHTTPServer(httpd_port=8127, config={'tool': tool}) + self._tool.user.open_url(httpd.url()) + + print "Local HTTP server started." + httpd.serve_forever() diff --git a/Tools/Scripts/webkitpy/tool/commands/openbugs.py b/Tools/Scripts/webkitpy/tool/commands/openbugs.py new file mode 100644 index 000000000..1b51c9ff6 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/openbugs.py @@ -0,0 +1,63 @@ +# 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 re +import sys + +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand +from webkitpy.common.system.deprecated_logging import log + + +class OpenBugs(AbstractDeclarativeCommand): + name = "open-bugs" + help_text = "Finds all bug numbers passed in arguments (or stdin if no args provided) and opens them in a web browser" + + bug_number_regexp = re.compile(r"\b\d{4,6}\b") + + def _open_bugs(self, bug_ids): + for bug_id in bug_ids: + bug_url = self._tool.bugs.bug_url_for_bug_id(bug_id) + self._tool.user.open_url(bug_url) + + # _find_bugs_in_string mostly exists for easy unit testing. + def _find_bugs_in_string(self, string): + return self.bug_number_regexp.findall(string) + + def _find_bugs_in_iterable(self, iterable): + return sum([self._find_bugs_in_string(string) for string in iterable], []) + + def execute(self, options, args, tool): + if args: + bug_ids = self._find_bugs_in_iterable(args) + else: + # This won't open bugs until stdin is closed but could be made to easily. That would just make unit testing slightly harder. + bug_ids = self._find_bugs_in_iterable(sys.stdin) + + log("%s bugs found in input." % len(bug_ids)) + + self._open_bugs(bug_ids) diff --git a/Tools/Scripts/webkitpy/tool/commands/openbugs_unittest.py b/Tools/Scripts/webkitpy/tool/commands/openbugs_unittest.py new file mode 100644 index 000000000..40a6e1b2e --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/openbugs_unittest.py @@ -0,0 +1,50 @@ +# Copyright (C) 2009 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. + +from webkitpy.tool.commands.commandtest import CommandsTest +from webkitpy.tool.commands.openbugs import OpenBugs + +class OpenBugsTest(CommandsTest): + + find_bugs_in_string_expectations = [ + ["123", []], + ["1234", ["1234"]], + ["12345", ["12345"]], + ["123456", ["123456"]], + ["1234567", []], + [" 123456 234567", ["123456", "234567"]], + ] + + def test_find_bugs_in_string(self): + openbugs = OpenBugs() + for expectation in self.find_bugs_in_string_expectations: + self.assertEquals(openbugs._find_bugs_in_string(expectation[0]), expectation[1]) + + def test_args_parsing(self): + expected_stderr = "2 bugs found in input.\nMOCK: user.open_url: http://example.com/12345\nMOCK: user.open_url: http://example.com/23456\n" + self.assert_execute_outputs(OpenBugs(), ["12345\n23456"], expected_stderr=expected_stderr) diff --git a/Tools/Scripts/webkitpy/tool/commands/prettydiff.py b/Tools/Scripts/webkitpy/tool/commands/prettydiff.py new file mode 100644 index 000000000..66a06a69e --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/prettydiff.py @@ -0,0 +1,39 @@ +# 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. + +from webkitpy.tool.commands.abstractsequencedcommand import AbstractSequencedCommand +from webkitpy.tool import steps + + +class PrettyDiff(AbstractSequencedCommand): + name = "pretty-diff" + help_text = "Shows the pretty diff in the default browser" + show_in_main_help = True + steps = [ + steps.ConfirmDiff, + ] diff --git a/Tools/Scripts/webkitpy/tool/commands/queries.py b/Tools/Scripts/webkitpy/tool/commands/queries.py new file mode 100644 index 000000000..7d23717a8 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/queries.py @@ -0,0 +1,407 @@ +# Copyright (c) 2009 Google Inc. All rights reserved. +# Copyright (c) 2009 Apple 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. + + +from optparse import make_option + +from webkitpy.tool import steps + +from webkitpy.common.checkout.commitinfo import CommitInfo +from webkitpy.common.config.committers import CommitterList +import webkitpy.common.config.urls as config_urls +from webkitpy.common.net.buildbot import BuildBot +from webkitpy.common.net.regressionwindow import RegressionWindow +from webkitpy.common.system.crashlogs import CrashLogs +from webkitpy.common.system.user import User +from webkitpy.tool.grammar import pluralize +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand +from webkitpy.common.system.deprecated_logging import log +from webkitpy.layout_tests import port + + +class SuggestReviewers(AbstractDeclarativeCommand): + name = "suggest-reviewers" + help_text = "Suggest reviewers for a patch based on recent changes to the modified files." + + def __init__(self): + options = [ + steps.Options.git_commit, + ] + AbstractDeclarativeCommand.__init__(self, options=options) + + def execute(self, options, args, tool): + reviewers = tool.checkout().suggested_reviewers(options.git_commit) + print "\n".join([reviewer.full_name for reviewer in reviewers]) + + +class BugsToCommit(AbstractDeclarativeCommand): + name = "bugs-to-commit" + help_text = "List bugs in the commit-queue" + + def execute(self, options, args, tool): + # FIXME: This command is poorly named. It's fetching the commit-queue list here. The name implies it's fetching pending-commit (all r+'d patches). + bug_ids = tool.bugs.queries.fetch_bug_ids_from_commit_queue() + for bug_id in bug_ids: + print "%s" % bug_id + + +class PatchesInCommitQueue(AbstractDeclarativeCommand): + name = "patches-in-commit-queue" + help_text = "List patches in the commit-queue" + + def execute(self, options, args, tool): + patches = tool.bugs.queries.fetch_patches_from_commit_queue() + log("Patches in commit queue:") + for patch in patches: + print patch.url() + + +class PatchesToCommitQueue(AbstractDeclarativeCommand): + name = "patches-to-commit-queue" + help_text = "Patches which should be added to the commit queue" + def __init__(self): + options = [ + make_option("--bugs", action="store_true", dest="bugs", help="Output bug links instead of patch links"), + ] + AbstractDeclarativeCommand.__init__(self, options=options) + + @staticmethod + def _needs_commit_queue(patch): + if patch.commit_queue() == "+": # If it's already cq+, ignore the patch. + log("%s already has cq=%s" % (patch.id(), patch.commit_queue())) + return False + + # We only need to worry about patches from contributers who are not yet committers. + committer_record = CommitterList().committer_by_email(patch.attacher_email()) + if committer_record: + log("%s committer = %s" % (patch.id(), committer_record)) + return not committer_record + + def execute(self, options, args, tool): + patches = tool.bugs.queries.fetch_patches_from_pending_commit_list() + patches_needing_cq = filter(self._needs_commit_queue, patches) + if options.bugs: + bugs_needing_cq = map(lambda patch: patch.bug_id(), patches_needing_cq) + bugs_needing_cq = sorted(set(bugs_needing_cq)) + for bug_id in bugs_needing_cq: + print "%s" % tool.bugs.bug_url_for_bug_id(bug_id) + else: + for patch in patches_needing_cq: + print "%s" % tool.bugs.attachment_url_for_id(patch.id(), action="edit") + + +class PatchesToReview(AbstractDeclarativeCommand): + name = "patches-to-review" + help_text = "List patches that are pending review" + + def execute(self, options, args, tool): + patch_ids = tool.bugs.queries.fetch_attachment_ids_from_review_queue() + log("Patches pending review:") + for patch_id in patch_ids: + print patch_id + + +class LastGreenRevision(AbstractDeclarativeCommand): + name = "last-green-revision" + help_text = "Prints the last known good revision" + + def execute(self, options, args, tool): + print self._tool.buildbot.last_green_revision() + + +class WhatBroke(AbstractDeclarativeCommand): + name = "what-broke" + help_text = "Print failing buildbots (%s) and what revisions broke them" % config_urls.buildbot_url + + def _print_builder_line(self, builder_name, max_name_width, status_message): + print "%s : %s" % (builder_name.ljust(max_name_width), status_message) + + def _print_blame_information_for_builder(self, builder_status, name_width, avoid_flakey_tests=True): + builder = self._tool.buildbot.builder_with_name(builder_status["name"]) + red_build = builder.build(builder_status["build_number"]) + regression_window = builder.find_regression_window(red_build) + if not regression_window.failing_build(): + self._print_builder_line(builder.name(), name_width, "FAIL (error loading build information)") + return + if not regression_window.build_before_failure(): + self._print_builder_line(builder.name(), name_width, "FAIL (blame-list: sometime before %s?)" % regression_window.failing_build().revision()) + return + + revisions = regression_window.revisions() + first_failure_message = "" + if (regression_window.failing_build() == builder.build(builder_status["build_number"])): + first_failure_message = " FIRST FAILURE, possibly a flaky test" + self._print_builder_line(builder.name(), name_width, "FAIL (blame-list: %s%s)" % (revisions, first_failure_message)) + for revision in revisions: + commit_info = self._tool.checkout().commit_info_for_revision(revision) + if commit_info: + print commit_info.blame_string(self._tool.bugs) + else: + print "FAILED to fetch CommitInfo for r%s, likely missing ChangeLog" % revision + + def execute(self, options, args, tool): + builder_statuses = tool.buildbot.builder_statuses() + longest_builder_name = max(map(len, map(lambda builder: builder["name"], builder_statuses))) + failing_builders = 0 + for builder_status in builder_statuses: + # If the builder is green, print OK, exit. + if builder_status["is_green"]: + continue + self._print_blame_information_for_builder(builder_status, name_width=longest_builder_name) + failing_builders += 1 + if failing_builders: + print "%s of %s are failing" % (failing_builders, pluralize("builder", len(builder_statuses))) + else: + print "All builders are passing!" + + +class ResultsFor(AbstractDeclarativeCommand): + name = "results-for" + help_text = "Print a list of failures for the passed revision from bots on %s" % config_urls.buildbot_url + argument_names = "REVISION" + + def _print_layout_test_results(self, results): + if not results: + print " No results." + return + for title, files in results.parsed_results().items(): + print " %s" % title + for filename in files: + print " %s" % filename + + def execute(self, options, args, tool): + builders = self._tool.buildbot.builders() + for builder in builders: + print "%s:" % builder.name() + build = builder.build_for_revision(args[0], allow_failed_lookups=True) + self._print_layout_test_results(build.layout_test_results()) + + +class FailureReason(AbstractDeclarativeCommand): + name = "failure-reason" + help_text = "Lists revisions where individual test failures started at %s" % config_urls.buildbot_url + + def _blame_line_for_revision(self, revision): + try: + commit_info = self._tool.checkout().commit_info_for_revision(revision) + except Exception, e: + return "FAILED to fetch CommitInfo for r%s, exception: %s" % (revision, e) + if not commit_info: + return "FAILED to fetch CommitInfo for r%s, likely missing ChangeLog" % revision + return commit_info.blame_string(self._tool.bugs) + + def _print_blame_information_for_transition(self, regression_window, failing_tests): + red_build = regression_window.failing_build() + print "SUCCESS: Build %s (r%s) was the first to show failures: %s" % (red_build._number, red_build.revision(), failing_tests) + print "Suspect revisions:" + for revision in regression_window.revisions(): + print self._blame_line_for_revision(revision) + + def _explain_failures_for_builder(self, builder, start_revision): + print "Examining failures for \"%s\", starting at r%s" % (builder.name(), start_revision) + revision_to_test = start_revision + build = builder.build_for_revision(revision_to_test, allow_failed_lookups=True) + layout_test_results = build.layout_test_results() + if not layout_test_results: + # FIXME: This could be made more user friendly. + print "Failed to load layout test results from %s; can't continue. (start revision = r%s)" % (build.results_url(), start_revision) + return 1 + + results_to_explain = set(layout_test_results.failing_tests()) + last_build_with_results = build + print "Starting at %s" % revision_to_test + while results_to_explain: + revision_to_test -= 1 + new_build = builder.build_for_revision(revision_to_test, allow_failed_lookups=True) + if not new_build: + print "No build for %s" % revision_to_test + continue + build = new_build + latest_results = build.layout_test_results() + if not latest_results: + print "No results build %s (r%s)" % (build._number, build.revision()) + continue + failures = set(latest_results.failing_tests()) + if len(failures) >= 20: + # FIXME: We may need to move this logic into the LayoutTestResults class. + # The buildbot stops runs after 20 failures so we don't have full results to work with here. + print "Too many failures in build %s (r%s), ignoring." % (build._number, build.revision()) + continue + fixed_results = results_to_explain - failures + if not fixed_results: + print "No change in build %s (r%s), %s unexplained failures (%s in this build)" % (build._number, build.revision(), len(results_to_explain), len(failures)) + last_build_with_results = build + continue + regression_window = RegressionWindow(build, last_build_with_results) + self._print_blame_information_for_transition(regression_window, fixed_results) + last_build_with_results = build + results_to_explain -= fixed_results + if results_to_explain: + print "Failed to explain failures: %s" % results_to_explain + return 1 + print "Explained all results for %s" % builder.name() + return 0 + + def _builder_to_explain(self): + builder_statuses = self._tool.buildbot.builder_statuses() + red_statuses = [status for status in builder_statuses if not status["is_green"]] + print "%s failing" % (pluralize("builder", len(red_statuses))) + builder_choices = [status["name"] for status in red_statuses] + # We could offer an "All" choice here. + chosen_name = self._tool.user.prompt_with_list("Which builder to diagnose:", builder_choices) + # FIXME: prompt_with_list should really take a set of objects and a set of names and then return the object. + for status in red_statuses: + if status["name"] == chosen_name: + return (self._tool.buildbot.builder_with_name(chosen_name), status["built_revision"]) + + def execute(self, options, args, tool): + (builder, latest_revision) = self._builder_to_explain() + start_revision = self._tool.user.prompt("Revision to walk backwards from? [%s] " % latest_revision) or latest_revision + if not start_revision: + print "Revision required." + return 1 + return self._explain_failures_for_builder(builder, start_revision=int(start_revision)) + + +class FindFlakyTests(AbstractDeclarativeCommand): + name = "find-flaky-tests" + help_text = "Lists tests that often fail for a single build at %s" % config_urls.buildbot_url + + def _find_failures(self, builder, revision): + build = builder.build_for_revision(revision, allow_failed_lookups=True) + if not build: + print "No build for %s" % revision + return (None, None) + results = build.layout_test_results() + if not results: + print "No results build %s (r%s)" % (build._number, build.revision()) + return (None, None) + failures = set(results.failing_tests()) + if len(failures) >= 20: + # FIXME: We may need to move this logic into the LayoutTestResults class. + # The buildbot stops runs after 20 failures so we don't have full results to work with here. + print "Too many failures in build %s (r%s), ignoring." % (build._number, build.revision()) + return (None, None) + return (build, failures) + + def _increment_statistics(self, flaky_tests, flaky_test_statistics): + for test in flaky_tests: + count = flaky_test_statistics.get(test, 0) + flaky_test_statistics[test] = count + 1 + + def _print_statistics(self, statistics): + print "=== Results ===" + print "Occurances Test name" + for value, key in sorted([(value, key) for key, value in statistics.items()]): + print "%10d %s" % (value, key) + + def _walk_backwards_from(self, builder, start_revision, limit): + flaky_test_statistics = {} + all_previous_failures = set([]) + one_time_previous_failures = set([]) + previous_build = None + for i in range(limit): + revision = start_revision - i + print "Analyzing %s ... " % revision, + (build, failures) = self._find_failures(builder, revision) + if failures == None: + # Notice that we don't loop on the empty set! + continue + print "has %s failures" % len(failures) + flaky_tests = one_time_previous_failures - failures + if flaky_tests: + print "Flaky tests: %s %s" % (sorted(flaky_tests), + previous_build.results_url()) + self._increment_statistics(flaky_tests, flaky_test_statistics) + one_time_previous_failures = failures - all_previous_failures + all_previous_failures = failures + previous_build = build + self._print_statistics(flaky_test_statistics) + + def _builder_to_analyze(self): + statuses = self._tool.buildbot.builder_statuses() + choices = [status["name"] for status in statuses] + chosen_name = self._tool.user.prompt_with_list("Which builder to analyze:", choices) + for status in statuses: + if status["name"] == chosen_name: + return (self._tool.buildbot.builder_with_name(chosen_name), status["built_revision"]) + + def execute(self, options, args, tool): + (builder, latest_revision) = self._builder_to_analyze() + limit = self._tool.user.prompt("How many revisions to look through? [10000] ") or 10000 + return self._walk_backwards_from(builder, latest_revision, limit=int(limit)) + + +class TreeStatus(AbstractDeclarativeCommand): + name = "tree-status" + help_text = "Print the status of the %s buildbots" % config_urls.buildbot_url + long_help = """Fetches build status from http://build.webkit.org/one_box_per_builder +and displayes the status of each builder.""" + + def execute(self, options, args, tool): + for builder in tool.buildbot.builder_statuses(): + status_string = "ok" if builder["is_green"] else "FAIL" + print "%s : %s" % (status_string.ljust(4), builder["name"]) + + +class CrashLog(AbstractDeclarativeCommand): + name = "crash-log" + help_text = "Print the newest crash log for the given process" + long_help = """Finds the newest crash log matching the given process name +and PID and prints it to stdout.""" + argument_names = "PROCESS_NAME [PID]" + + def execute(self, options, args, tool): + crash_logs = CrashLogs(tool.filesystem) + pid = None + if len(args) > 1: + pid = int(args[1]) + print crash_logs.find_newest_log(args[0], pid) + + +class SkippedPorts(AbstractDeclarativeCommand): + name = "skipped-ports" + help_text = "Print the list of ports skipping the given layout test(s)" + long_help = """Scans the the Skipped file of each port and figure +out what ports are skipping the test(s). Categories are taken in account too.""" + argument_names = "TEST_NAME" + + def execute(self, options, args, tool): + results = dict([(test_name, []) for test_name in args]) + for port_name in tool.port_factory.all_port_names(): + port_object = tool.port_factory.get(port_name) + for test_name in args: + if port_object.skips_layout_test(test_name): + results[test_name].append(port_name) + + for test_name, ports in results.iteritems(): + if ports: + print "Ports skipping test %r: %s" % (test_name, ', '.join(ports)) + else: + print "Test %r is not skipped by any port." % test_name diff --git a/Tools/Scripts/webkitpy/tool/commands/queries_unittest.py b/Tools/Scripts/webkitpy/tool/commands/queries_unittest.py new file mode 100644 index 000000000..fe13a4c54 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/queries_unittest.py @@ -0,0 +1,117 @@ +# Copyright (C) 2009 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.bugzilla import Bugzilla +from webkitpy.thirdparty.mock import Mock +from webkitpy.tool.commands.commandtest import CommandsTest +from webkitpy.tool.commands.queries import * +from webkitpy.tool.mocktool import MockTool + + +class MockTestPort1(object): + def skips_layout_test(self, test_name): + return test_name in ["media/foo/bar.html", "foo"] + + +class MockTestPort2(object): + def skips_layout_test(self, test_name): + return test_name == "media/foo/bar.html" + + +class MockPortFactory(object): + def __init__(self): + self._all_ports = { + "test_port1": MockTestPort1(), + "test_port2": MockTestPort2(), + } + + def all_port_names(self, options=None): + return self._all_ports.keys() + + def get(self, port_name): + return self._all_ports.get(port_name) + + +class QueryCommandsTest(CommandsTest): + def test_bugs_to_commit(self): + expected_stderr = "Warning, attachment 10001 on bug 50000 has invalid committer (non-committer@example.com)\n" + self.assert_execute_outputs(BugsToCommit(), None, "50000\n50003\n", expected_stderr) + + def test_patches_in_commit_queue(self): + expected_stdout = "http://example.com/10000\nhttp://example.com/10002\n" + expected_stderr = "Warning, attachment 10001 on bug 50000 has invalid committer (non-committer@example.com)\nPatches in commit queue:\n" + self.assert_execute_outputs(PatchesInCommitQueue(), None, expected_stdout, expected_stderr) + + def test_patches_to_commit_queue(self): + expected_stdout = "http://example.com/10003&action=edit\n" + expected_stderr = "10000 already has cq=+\n10001 already has cq=+\n10004 committer = \"Eric Seidel\" <eric@webkit.org>\n" + options = Mock() + options.bugs = False + self.assert_execute_outputs(PatchesToCommitQueue(), None, expected_stdout, expected_stderr, options=options) + + expected_stdout = "http://example.com/50003\n" + options.bugs = True + self.assert_execute_outputs(PatchesToCommitQueue(), None, expected_stdout, expected_stderr, options=options) + + def test_patches_to_review(self): + expected_stdout = "10002\n" + expected_stderr = "Patches pending review:\n" + self.assert_execute_outputs(PatchesToReview(), None, expected_stdout, expected_stderr) + + def test_tree_status(self): + expected_stdout = "ok : Builder1\nok : Builder2\n" + self.assert_execute_outputs(TreeStatus(), None, expected_stdout) + + def test_skipped_ports(self): + tool = MockTool() + tool.port_factory = MockPortFactory() + + expected_stdout = "Ports skipping test 'media/foo/bar.html': test_port1, test_port2\n" + self.assert_execute_outputs(SkippedPorts(), ("media/foo/bar.html",), expected_stdout, tool=tool) + + expected_stdout = "Ports skipping test 'foo': test_port1\n" + self.assert_execute_outputs(SkippedPorts(), ("foo",), expected_stdout, tool=tool) + + expected_stdout = "Test 'media' is not skipped by any port.\n" + self.assert_execute_outputs(SkippedPorts(), ("media",), expected_stdout, tool=tool) + + +class FailureReasonTest(unittest.TestCase): + def test_blame_line_for_revision(self): + tool = MockTool() + command = FailureReason() + command.bind_to_tool(tool) + # This is an artificial example, mostly to test the CommitInfo lookup failure case. + self.assertEquals(command._blame_line_for_revision(None), "FAILED to fetch CommitInfo for rNone, likely missing ChangeLog") + + def raising_mock(self): + raise Exception("MESSAGE") + tool.checkout().commit_info_for_revision = raising_mock + self.assertEquals(command._blame_line_for_revision(None), "FAILED to fetch CommitInfo for rNone, exception: MESSAGE") diff --git a/Tools/Scripts/webkitpy/tool/commands/queues.py b/Tools/Scripts/webkitpy/tool/commands/queues.py new file mode 100644 index 000000000..f61a63991 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/queues.py @@ -0,0 +1,443 @@ +# Copyright (c) 2009 Google Inc. All rights reserved. +# Copyright (c) 2009 Apple 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. + +from __future__ import with_statement + +import codecs +import time +import traceback +import os + +from datetime import datetime +from optparse import make_option +from StringIO import StringIO + +from webkitpy.common.config.committervalidator import CommitterValidator +from webkitpy.common.net.bugzilla import Attachment +from webkitpy.common.net.statusserver import StatusServer +from webkitpy.common.system.deprecated_logging import error, log +from webkitpy.common.system.executive import ScriptError +from webkitpy.tool.bot.botinfo import BotInfo +from webkitpy.tool.bot.commitqueuetask import CommitQueueTask, CommitQueueTaskDelegate +from webkitpy.tool.bot.expectedfailures import ExpectedFailures +from webkitpy.tool.bot.feeders import CommitQueueFeeder, EWSFeeder +from webkitpy.tool.bot.layouttestresultsreader import LayoutTestResultsReader +from webkitpy.tool.bot.queueengine import QueueEngine, QueueEngineDelegate +from webkitpy.tool.bot.flakytestreporter import FlakyTestReporter +from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler +from webkitpy.tool.multicommandtool import Command, TryAgain + + +class AbstractQueue(Command, QueueEngineDelegate): + watchers = [ + ] + + _pass_status = "Pass" + _fail_status = "Fail" + _retry_status = "Retry" + _error_status = "Error" + + def __init__(self, options=None): # Default values should never be collections (like []) as default values are shared between invocations + options_list = (options or []) + [ + make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Do not ask the user for confirmation before running the queue. Dangerous!"), + make_option("--exit-after-iteration", action="store", type="int", dest="iterations", default=None, help="Stop running the queue after iterating this number of times."), + ] + Command.__init__(self, "Run the %s" % self.name, options=options_list) + self._iteration_count = 0 + + def _cc_watchers(self, bug_id): + try: + self._tool.bugs.add_cc_to_bug(bug_id, self.watchers) + except Exception, e: + traceback.print_exc() + log("Failed to CC watchers.") + + def run_webkit_patch(self, args): + webkit_patch_args = [self._tool.path()] + # FIXME: This is a hack, we should have a more general way to pass global options. + # FIXME: We must always pass global options and their value in one argument + # because our global option code looks for the first argument which does + # not begin with "-" and assumes that is the command name. + webkit_patch_args += ["--status-host=%s" % self._tool.status_server.host] + if self._tool.status_server.bot_id: + webkit_patch_args += ["--bot-id=%s" % self._tool.status_server.bot_id] + if self._options.port: + webkit_patch_args += ["--port=%s" % self._options.port] + webkit_patch_args.extend(args) + # FIXME: There is probably no reason to use run_and_throw_if_fail anymore. + # run_and_throw_if_fail was invented to support tee'd output + # (where we write both to a log file and to the console at once), + # but the queues don't need live-progress, a dump-of-output at the + # end should be sufficient. + return self._tool.executive.run_and_throw_if_fail(webkit_patch_args, cwd=self._tool.scm().checkout_root) + + def _log_directory(self): + return os.path.join("..", "%s-logs" % self.name) + + # QueueEngineDelegate methods + + def queue_log_path(self): + return os.path.join(self._log_directory(), "%s.log" % self.name) + + def work_item_log_path(self, work_item): + raise NotImplementedError, "subclasses must implement" + + def begin_work_queue(self): + log("CAUTION: %s will discard all local changes in \"%s\"" % (self.name, self._tool.scm().checkout_root)) + if self._options.confirm: + response = self._tool.user.prompt("Are you sure? Type \"yes\" to continue: ") + if (response != "yes"): + error("User declined.") + log("Running WebKit %s." % self.name) + self._tool.status_server.update_status(self.name, "Starting Queue") + + def stop_work_queue(self, reason): + self._tool.status_server.update_status(self.name, "Stopping Queue, reason: %s" % reason) + + def should_continue_work_queue(self): + self._iteration_count += 1 + return not self._options.iterations or self._iteration_count <= self._options.iterations + + def next_work_item(self): + raise NotImplementedError, "subclasses must implement" + + def should_proceed_with_work_item(self, work_item): + raise NotImplementedError, "subclasses must implement" + + def process_work_item(self, work_item): + raise NotImplementedError, "subclasses must implement" + + def handle_unexpected_error(self, work_item, message): + raise NotImplementedError, "subclasses must implement" + + # Command methods + + def execute(self, options, args, tool, engine=QueueEngine): + self._options = options # FIXME: This code is wrong. Command.options is a list, this assumes an Options element! + self._tool = tool # FIXME: This code is wrong too! Command.bind_to_tool handles this! + return engine(self.name, self, self._tool.wakeup_event).run() + + @classmethod + def _log_from_script_error_for_upload(cls, script_error, output_limit=None): + # We have seen request timeouts with app engine due to large + # log uploads. Trying only the last 512k. + if not output_limit: + output_limit = 512 * 1024 # 512k + output = script_error.message_with_output(output_limit=output_limit) + # We pre-encode the string to a byte array before passing it + # to status_server, because ClientForm (part of mechanize) + # wants a file-like object with pre-encoded data. + return StringIO(output.encode("utf-8")) + + @classmethod + def _update_status_for_script_error(cls, tool, state, script_error, is_error=False): + message = str(script_error) + if is_error: + message = "Error: %s" % message + failure_log = cls._log_from_script_error_for_upload(script_error) + return tool.status_server.update_status(cls.name, message, state["patch"], failure_log) + + +class FeederQueue(AbstractQueue): + name = "feeder-queue" + + _sleep_duration = 30 # seconds + + # AbstractQueue methods + + def begin_work_queue(self): + AbstractQueue.begin_work_queue(self) + self.feeders = [ + CommitQueueFeeder(self._tool), + EWSFeeder(self._tool), + ] + + def next_work_item(self): + # This really show inherit from some more basic class that doesn't + # understand work items, but the base class in the heirarchy currently + # understands work items. + return "synthetic-work-item" + + def should_proceed_with_work_item(self, work_item): + return True + + def process_work_item(self, work_item): + for feeder in self.feeders: + feeder.feed() + time.sleep(self._sleep_duration) + return True + + def work_item_log_path(self, work_item): + return None + + def handle_unexpected_error(self, work_item, message): + log(message) + + +class AbstractPatchQueue(AbstractQueue): + def _update_status(self, message, patch=None, results_file=None): + return self._tool.status_server.update_status(self.name, message, patch, results_file) + + def _next_patch(self): + patch_id = self._tool.status_server.next_work_item(self.name) + if not patch_id: + return None + patch = self._tool.bugs.fetch_attachment(patch_id) + if not patch: + # FIXME: Using a fake patch because release_work_item has the wrong API. + # We also don't really need to release the lock (although that's fine), + # mostly we just need to remove this bogus patch from our queue. + # If for some reason bugzilla is just down, then it will be re-fed later. + patch = Attachment({'id': patch_id}, None) + self._release_work_item(patch) + return None + return patch + + def _release_work_item(self, patch): + self._tool.status_server.release_work_item(self.name, patch) + + def _did_pass(self, patch): + self._update_status(self._pass_status, patch) + self._release_work_item(patch) + + def _did_fail(self, patch): + self._update_status(self._fail_status, patch) + self._release_work_item(patch) + + def _did_retry(self, patch): + self._update_status(self._retry_status, patch) + self._release_work_item(patch) + + def _did_error(self, patch, reason): + message = "%s: %s" % (self._error_status, reason) + self._update_status(message, patch) + self._release_work_item(patch) + + # FIXME: This probably belongs at a layer below AbstractPatchQueue, but shared by CommitQueue and the EarlyWarningSystem. + def _upload_results_archive_for_patch(self, patch, results_archive_zip): + bot_id = self._tool.status_server.bot_id or "bot" + description = "Archive of layout-test-results from %s" % bot_id + # results_archive is a ZipFile object, grab the File object (.fp) to pass to Mechanize for uploading. + results_archive_file = results_archive_zip.fp + # Rewind the file object to start (since Mechanize won't do that automatically) + # See https://bugs.webkit.org/show_bug.cgi?id=54593 + results_archive_file.seek(0) + # FIXME: This is a small lie to always say run-webkit-tests since Chromium uses new-run-webkit-tests. + # We could make this code look up the test script name off the port. + comment_text = "The attached test failures were seen while running run-webkit-tests on the %s.\n" % (self.name) + # FIXME: We could easily list the test failures from the archive here, + # currently callers do that separately. + comment_text += BotInfo(self._tool).summary_text() + self._tool.bugs.add_attachment_to_bug(patch.bug_id(), results_archive_file, description, filename="layout-test-results.zip", comment_text=comment_text) + + def work_item_log_path(self, patch): + return os.path.join(self._log_directory(), "%s.log" % patch.bug_id()) + + +class CommitQueue(AbstractPatchQueue, StepSequenceErrorHandler, CommitQueueTaskDelegate): + name = "commit-queue" + + # AbstractPatchQueue methods + + def begin_work_queue(self): + AbstractPatchQueue.begin_work_queue(self) + self.committer_validator = CommitterValidator(self._tool) + self._expected_failures = ExpectedFailures() + self._layout_test_results_reader = LayoutTestResultsReader(self._tool, self._log_directory()) + + def next_work_item(self): + return self._next_patch() + + def should_proceed_with_work_item(self, patch): + patch_text = "rollout patch" if patch.is_rollout() else "patch" + self._update_status("Processing %s" % patch_text, patch) + return True + + def process_work_item(self, patch): + self._cc_watchers(patch.bug_id()) + task = CommitQueueTask(self, patch) + try: + if task.run(): + self._did_pass(patch) + return True + self._did_retry(patch) + except ScriptError, e: + validator = CommitterValidator(self._tool) + validator.reject_patch_from_commit_queue(patch.id(), self._error_message_for_bug(task, patch, e)) + results_archive = task.results_archive_from_patch_test_run(patch) + if results_archive: + self._upload_results_archive_for_patch(patch, results_archive) + self._did_fail(patch) + + def _failing_tests_message(self, task, patch): + results = task.results_from_patch_test_run(patch) + unexpected_failures = self._expected_failures.unexpected_failures_observed(results) + if not unexpected_failures: + return None + return "New failing tests:\n%s" % "\n".join(unexpected_failures) + + def _error_message_for_bug(self, task, patch, script_error): + message = self._failing_tests_message(task, patch) + if not message: + message = script_error.message_with_output() + results_link = self._tool.status_server.results_url_for_status(task.failure_status_id) + return "%s\nFull output: %s" % (message, results_link) + + def handle_unexpected_error(self, patch, message): + self.committer_validator.reject_patch_from_commit_queue(patch.id(), message) + + # CommitQueueTaskDelegate methods + + def run_command(self, command): + self.run_webkit_patch(command) + + def command_passed(self, message, patch): + self._update_status(message, patch=patch) + + def command_failed(self, message, script_error, patch): + failure_log = self._log_from_script_error_for_upload(script_error) + return self._update_status(message, patch=patch, results_file=failure_log) + + def expected_failures(self): + return self._expected_failures + + def layout_test_results(self): + return self._layout_test_results_reader.results() + + def archive_last_layout_test_results(self, patch): + return self._layout_test_results_reader.archive(patch) + + def build_style(self): + return "both" + + def refetch_patch(self, patch): + return self._tool.bugs.fetch_attachment(patch.id()) + + def report_flaky_tests(self, patch, flaky_test_results, results_archive=None): + reporter = FlakyTestReporter(self._tool, self.name) + reporter.report_flaky_tests(patch, flaky_test_results, results_archive) + + # StepSequenceErrorHandler methods + + def handle_script_error(cls, tool, state, script_error): + # Hitting this error handler should be pretty rare. It does occur, + # however, when a patch no longer applies to top-of-tree in the final + # land step. + log(script_error.message_with_output()) + + @classmethod + def handle_checkout_needs_update(cls, tool, state, options, error): + message = "Tests passed, but commit failed (checkout out of date). Updating, then landing without building or re-running tests." + tool.status_server.update_status(cls.name, message, state["patch"]) + # The only time when we find out that out checkout needs update is + # when we were ready to actually pull the trigger and land the patch. + # Rather than spinning in the master process, we retry without + # building or testing, which is much faster. + options.build = False + options.test = False + options.update = True + raise TryAgain() + + +class AbstractReviewQueue(AbstractPatchQueue, StepSequenceErrorHandler): + """This is the base-class for the EWS queues and the style-queue.""" + def __init__(self, options=None): + AbstractPatchQueue.__init__(self, options) + + def review_patch(self, patch): + raise NotImplementedError("subclasses must implement") + + # AbstractPatchQueue methods + + def begin_work_queue(self): + AbstractPatchQueue.begin_work_queue(self) + + def next_work_item(self): + return self._next_patch() + + def should_proceed_with_work_item(self, patch): + raise NotImplementedError("subclasses must implement") + + def process_work_item(self, patch): + try: + if not self.review_patch(patch): + return False + self._did_pass(patch) + return True + except ScriptError, e: + if e.exit_code != QueueEngine.handled_error_code: + self._did_fail(patch) + else: + # The subprocess handled the error, but won't have released the patch, so we do. + # FIXME: We need to simplify the rules by which _release_work_item is called. + self._release_work_item(patch) + raise e + + def handle_unexpected_error(self, patch, message): + log(message) + + # StepSequenceErrorHandler methods + + @classmethod + def handle_script_error(cls, tool, state, script_error): + log(script_error.message_with_output()) + + +class StyleQueue(AbstractReviewQueue): + name = "style-queue" + def __init__(self): + AbstractReviewQueue.__init__(self) + + def should_proceed_with_work_item(self, patch): + self._update_status("Checking style", patch) + return True + + def review_patch(self, patch): + try: + # Run the style checks. + self.run_webkit_patch(["check-style", "--force-clean", "--non-interactive", "--parent-command=style-queue", patch.id()]) + finally: + # Apply the watch list. + try: + self.run_webkit_patch(["apply-watchlist-local", patch.bug_id()]) + except ScriptError, e: + # Don't turn the style bot block red due to watchlist errors. + pass + + return True + + @classmethod + def handle_script_error(cls, tool, state, script_error): + is_svn_apply = script_error.command_name() == "svn-apply" + status_id = cls._update_status_for_script_error(tool, state, script_error, is_error=is_svn_apply) + if is_svn_apply: + QueueEngine.exit_after_handled_error(script_error) + message = "Attachment %s did not pass %s:\n\n%s\n\nIf any of these errors are false positives, please file a bug against check-webkit-style." % (state["patch"].id(), cls.name, script_error.message_with_output(output_limit=3*1024)) + tool.bugs.post_comment_to_bug(state["patch"].bug_id(), message, cc=cls.watchers) + exit(1) diff --git a/Tools/Scripts/webkitpy/tool/commands/queues_unittest.py b/Tools/Scripts/webkitpy/tool/commands/queues_unittest.py new file mode 100644 index 000000000..ae3bffee7 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/queues_unittest.py @@ -0,0 +1,486 @@ +# Copyright (C) 2009 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 os +import StringIO + +from webkitpy.common.checkout.scm import CheckoutNeedsUpdate +from webkitpy.common.checkout.scm.scm_mock import MockSCM +from webkitpy.common.net.bugzilla import Attachment +from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.layout_tests.models import test_results +from webkitpy.layout_tests.models import test_failures +from webkitpy.thirdparty.mock import Mock +from webkitpy.tool.commands.commandtest import CommandsTest +from webkitpy.tool.commands.queues import * +from webkitpy.tool.commands.queuestest import QueuesTest +from webkitpy.tool.commands.stepsequence import StepSequence +from webkitpy.common.net.statusserver_mock import MockStatusServer +from webkitpy.tool.mocktool import MockTool, MockOptions + + +class TestCommitQueue(CommitQueue): + def __init__(self, tool=None): + CommitQueue.__init__(self) + if tool: + self.bind_to_tool(tool) + self._options = MockOptions(confirm=False, parent_command="commit-queue") + + def begin_work_queue(self): + output_capture = OutputCapture() + output_capture.capture_output() + CommitQueue.begin_work_queue(self) + output_capture.restore_output() + + +class TestQueue(AbstractPatchQueue): + name = "test-queue" + + +class TestReviewQueue(AbstractReviewQueue): + name = "test-review-queue" + + +class TestFeederQueue(FeederQueue): + _sleep_duration = 0 + + +class AbstractQueueTest(CommandsTest): + def test_log_directory(self): + self.assertEquals(TestQueue()._log_directory(), os.path.join("..", "test-queue-logs")) + + def _assert_run_webkit_patch(self, run_args, port=None): + queue = TestQueue() + tool = MockTool() + tool.status_server.bot_id = "gort" + tool.executive = Mock() + queue.bind_to_tool(tool) + queue._options = Mock() + queue._options.port = port + + queue.run_webkit_patch(run_args) + expected_run_args = ["echo", "--status-host=example.com", "--bot-id=gort"] + if port: + expected_run_args.append("--port=%s" % port) + expected_run_args.extend(run_args) + tool.executive.run_and_throw_if_fail.assert_called_with(expected_run_args, cwd='/mock-checkout') + + def test_run_webkit_patch(self): + self._assert_run_webkit_patch([1]) + self._assert_run_webkit_patch(["one", 2]) + self._assert_run_webkit_patch([1], port="mockport") + + def test_iteration_count(self): + queue = TestQueue() + queue._options = Mock() + queue._options.iterations = 3 + self.assertTrue(queue.should_continue_work_queue()) + self.assertTrue(queue.should_continue_work_queue()) + self.assertTrue(queue.should_continue_work_queue()) + self.assertFalse(queue.should_continue_work_queue()) + + def test_no_iteration_count(self): + queue = TestQueue() + queue._options = Mock() + self.assertTrue(queue.should_continue_work_queue()) + self.assertTrue(queue.should_continue_work_queue()) + self.assertTrue(queue.should_continue_work_queue()) + self.assertTrue(queue.should_continue_work_queue()) + + def _assert_log_message(self, script_error, log_message): + failure_log = AbstractQueue._log_from_script_error_for_upload(script_error, output_limit=10) + self.assertTrue(failure_log.read(), log_message) + + def test_log_from_script_error_for_upload(self): + self._assert_log_message(ScriptError("test"), "test") + # In python 2.5 unicode(Exception) is busted. See: + # http://bugs.python.org/issue2517 + # With no good workaround, we just ignore these tests. + if not hasattr(Exception, "__unicode__"): + return + + unicode_tor = u"WebKit \u2661 Tor Arne Vestb\u00F8!" + utf8_tor = unicode_tor.encode("utf-8") + self._assert_log_message(ScriptError(unicode_tor), utf8_tor) + script_error = ScriptError(unicode_tor, output=unicode_tor) + expected_output = "%s\nLast %s characters of output:\n%s" % (utf8_tor, 10, utf8_tor[-10:]) + self._assert_log_message(script_error, expected_output) + + +class FeederQueueTest(QueuesTest): + def test_feeder_queue(self): + queue = TestFeederQueue() + tool = MockTool(log_executive=True) + expected_stderr = { + "begin_work_queue": self._default_begin_work_queue_stderr("feeder-queue"), + "should_proceed_with_work_item": "", + "next_work_item": "", + "process_work_item": """Warning, attachment 10001 on bug 50000 has invalid committer (non-committer@example.com) +Warning, attachment 10001 on bug 50000 has invalid committer (non-committer@example.com) +MOCK setting flag 'commit-queue' to '-' on attachment '10001' with comment 'Rejecting attachment 10001 from commit-queue.' and additional comment 'non-committer@example.com does not have committer permissions according to http://trac.webkit.org/browser/trunk/Tools/Scripts/webkitpy/common/config/committers.py. + +- If you do not have committer rights please read http://webkit.org/coding/contributing.html for instructions on how to use bugzilla flags. + +- If you have committer rights please correct the error in Tools/Scripts/webkitpy/common/config/committers.py by adding yourself to the file (no review needed). The commit-queue restarts itself every 2 hours. After restart the commit-queue will correctly respect your committer rights.' +MOCK: update_work_items: commit-queue [10005, 10000] +Feeding commit-queue items [10005, 10000] +Feeding EWS (1 r? patch, 1 new) +MOCK: submit_to_ews: 10002 +""", + "handle_unexpected_error": "Mock error message\n", + } + self.assert_queue_outputs(queue, tool=tool, expected_stderr=expected_stderr) + + +class AbstractPatchQueueTest(CommandsTest): + def test_next_patch(self): + queue = AbstractPatchQueue() + tool = MockTool() + queue.bind_to_tool(tool) + queue._options = Mock() + queue._options.port = None + self.assertEquals(queue._next_patch(), None) + tool.status_server = MockStatusServer(work_items=[2, 10000]) + expected_stdout = "MOCK: fetch_attachment: 2 is not a known attachment id\n" # A mock-only message to prevent us from making mistakes. + expected_stderr = "MOCK: release_work_item: None 2\n" + patch_id = OutputCapture().assert_outputs(self, queue._next_patch, expected_stdout=expected_stdout, expected_stderr=expected_stderr) + self.assertEquals(patch_id, None) # 2 is an invalid patch id + self.assertEquals(queue._next_patch().id(), 10000) + + def test_upload_results_archive_for_patch(self): + queue = AbstractPatchQueue() + queue.name = "mock-queue" + tool = MockTool() + queue.bind_to_tool(tool) + queue._options = Mock() + queue._options.port = None + patch = queue._tool.bugs.fetch_attachment(10001) + expected_stderr = """MOCK add_attachment_to_bug: bug_id=50000, description=Archive of layout-test-results from bot filename=layout-test-results.zip +-- Begin comment -- +The attached test failures were seen while running run-webkit-tests on the mock-queue. +Port: MockPort Platform: MockPlatform 1.0 +-- End comment -- +""" + OutputCapture().assert_outputs(self, queue._upload_results_archive_for_patch, [patch, Mock()], expected_stderr=expected_stderr) + + +class NeedsUpdateSequence(StepSequence): + def _run(self, tool, options, state): + raise CheckoutNeedsUpdate([], 1, "", None) + + +class AlwaysCommitQueueTool(object): + def __init__(self): + self.status_server = MockStatusServer() + + def command_by_name(self, name): + return CommitQueue + + +class SecondThoughtsCommitQueue(TestCommitQueue): + def __init__(self, tool=None): + self._reject_patch = False + TestCommitQueue.__init__(self, tool) + + def run_command(self, command): + # We want to reject the patch after the first validation, + # so wait to reject it until after some other command has run. + self._reject_patch = True + return CommitQueue.run_command(self, command) + + def refetch_patch(self, patch): + if not self._reject_patch: + return self._tool.bugs.fetch_attachment(patch.id()) + + attachment_dictionary = { + "id": patch.id(), + "bug_id": patch.bug_id(), + "name": "Rejected", + "is_obsolete": True, + "is_patch": False, + "review": "-", + "reviewer_email": "foo@bar.com", + "commit-queue": "-", + "committer_email": "foo@bar.com", + "attacher_email": "Contributer1", + } + return Attachment(attachment_dictionary, None) + + +class CommitQueueTest(QueuesTest): + def _mock_test_result(self, testname): + return test_results.TestResult(testname, [test_failures.FailureTextMismatch()]) + + def test_commit_queue(self): + expected_stderr = { + "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue"), + "should_proceed_with_work_item": "MOCK: update_status: commit-queue Processing patch\n", + "next_work_item": "", + "process_work_item": """MOCK: update_status: commit-queue Cleaned working directory +MOCK: update_status: commit-queue Updated working directory +MOCK: update_status: commit-queue Applied patch +MOCK: update_status: commit-queue ChangeLog validated +MOCK: update_status: commit-queue Built patch +MOCK: update_status: commit-queue Passed tests +MOCK: update_status: commit-queue Landed patch +MOCK: update_status: commit-queue Pass +MOCK: release_work_item: commit-queue 10000 +""", + "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '10000' with comment 'Rejecting attachment 10000 from commit-queue.' and additional comment 'Mock error message'\n", + "handle_script_error": "ScriptError error message\n", + } + self.assert_queue_outputs(CommitQueue(), expected_stderr=expected_stderr) + + def test_commit_queue_failure(self): + expected_stderr = { + "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue"), + "should_proceed_with_work_item": "MOCK: update_status: commit-queue Processing patch\n", + "next_work_item": "", + "process_work_item": """MOCK: update_status: commit-queue Cleaned working directory +MOCK: update_status: commit-queue Updated working directory +MOCK: update_status: commit-queue Patch does not apply +MOCK setting flag 'commit-queue' to '-' on attachment '10000' with comment 'Rejecting attachment 10000 from commit-queue.' and additional comment 'MOCK script error +Full output: http://dummy_url' +MOCK: update_status: commit-queue Fail +MOCK: release_work_item: commit-queue 10000 +""", + "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '10000' with comment 'Rejecting attachment 10000 from commit-queue.' and additional comment 'Mock error message'\n", + "handle_script_error": "ScriptError error message\n", + } + queue = CommitQueue() + + def mock_run_webkit_patch(command): + if command == ['clean'] or command == ['update']: + # We want cleaning to succeed so we can error out on a step + # that causes the commit-queue to reject the patch. + return + raise ScriptError('MOCK script error') + + queue.run_webkit_patch = mock_run_webkit_patch + self.assert_queue_outputs(queue, expected_stderr=expected_stderr) + + def test_commit_queue_failure_with_failing_tests(self): + expected_stderr = { + "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue"), + "should_proceed_with_work_item": "MOCK: update_status: commit-queue Processing patch\n", + "next_work_item": "", + "process_work_item": """MOCK: update_status: commit-queue Cleaned working directory +MOCK: update_status: commit-queue Updated working directory +MOCK: update_status: commit-queue Patch does not apply +MOCK setting flag 'commit-queue' to '-' on attachment '10000' with comment 'Rejecting attachment 10000 from commit-queue.' and additional comment 'New failing tests: +mock_test_name.html +another_test_name.html +Full output: http://dummy_url' +MOCK: update_status: commit-queue Fail +MOCK: release_work_item: commit-queue 10000 +""", + "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '10000' with comment 'Rejecting attachment 10000 from commit-queue.' and additional comment 'Mock error message'\n", + "handle_script_error": "ScriptError error message\n", + } + queue = CommitQueue() + + def mock_run_webkit_patch(command): + if command == ['clean'] or command == ['update']: + # We want cleaning to succeed so we can error out on a step + # that causes the commit-queue to reject the patch. + return + queue._expected_failures.unexpected_failures_observed = lambda results: ["mock_test_name.html", "another_test_name.html"] + raise ScriptError('MOCK script error') + + queue.run_webkit_patch = mock_run_webkit_patch + self.assert_queue_outputs(queue, expected_stderr=expected_stderr) + + def test_rollout(self): + tool = MockTool(log_executive=True) + tool.filesystem.write_text_file('/mock-results/results.html', '') # Otherwise the commit-queue will hit a KeyError trying to read the results from the MockFileSystem. + tool.buildbot.light_tree_on_fire() + expected_stderr = { + "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue"), + "should_proceed_with_work_item": "MOCK: update_status: commit-queue Processing patch\n", + "next_work_item": "", + "process_work_item": """MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'clean'], cwd=/mock-checkout +MOCK: update_status: commit-queue Cleaned working directory +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'update'], cwd=/mock-checkout +MOCK: update_status: commit-queue Updated working directory +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'apply-attachment', '--no-update', '--non-interactive', 10000], cwd=/mock-checkout +MOCK: update_status: commit-queue Applied patch +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'validate-changelog', '--non-interactive', 10000], cwd=/mock-checkout +MOCK: update_status: commit-queue ChangeLog validated +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build', '--no-clean', '--no-update', '--build-style=both'], cwd=/mock-checkout +MOCK: update_status: commit-queue Built patch +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive'], cwd=/mock-checkout +MOCK: update_status: commit-queue Passed tests +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'land-attachment', '--force-clean', '--non-interactive', '--parent-command=commit-queue', 10000], cwd=/mock-checkout +MOCK: update_status: commit-queue Landed patch +MOCK: update_status: commit-queue Pass +MOCK: release_work_item: commit-queue 10000 +""", + "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '10000' with comment 'Rejecting attachment 10000 from commit-queue.' and additional comment 'Mock error message'\n", + "handle_script_error": "ScriptError error message\n", + } + self.assert_queue_outputs(CommitQueue(), tool=tool, expected_stderr=expected_stderr) + + def test_rollout_lands(self): + tool = MockTool(log_executive=True) + tool.buildbot.light_tree_on_fire() + rollout_patch = tool.bugs.fetch_attachment(10005) # _patch6, a rollout patch. + assert(rollout_patch.is_rollout()) + expected_stderr = { + "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue"), + "should_proceed_with_work_item": "MOCK: update_status: commit-queue Processing rollout patch\n", + "next_work_item": "", + "process_work_item": """MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'clean'], cwd=/mock-checkout +MOCK: update_status: commit-queue Cleaned working directory +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'update'], cwd=/mock-checkout +MOCK: update_status: commit-queue Updated working directory +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'apply-attachment', '--no-update', '--non-interactive', 10005], cwd=/mock-checkout +MOCK: update_status: commit-queue Applied patch +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'validate-changelog', '--non-interactive', 10005], cwd=/mock-checkout +MOCK: update_status: commit-queue ChangeLog validated +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'land-attachment', '--force-clean', '--non-interactive', '--parent-command=commit-queue', 10005], cwd=/mock-checkout +MOCK: update_status: commit-queue Landed patch +MOCK: update_status: commit-queue Pass +MOCK: release_work_item: commit-queue 10005 +""", + "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '10005' with comment 'Rejecting attachment 10005 from commit-queue.' and additional comment 'Mock error message'\n", + "handle_script_error": "ScriptError error message\n", + } + self.assert_queue_outputs(CommitQueue(), tool=tool, work_item=rollout_patch, expected_stderr=expected_stderr) + + def test_auto_retry(self): + queue = CommitQueue() + options = Mock() + options.parent_command = "commit-queue" + tool = AlwaysCommitQueueTool() + sequence = NeedsUpdateSequence(None) + + expected_stderr = "Commit failed because the checkout is out of date. Please update and try again.\nMOCK: update_status: commit-queue Tests passed, but commit failed (checkout out of date). Updating, then landing without building or re-running tests.\n" + state = {'patch': None} + OutputCapture().assert_outputs(self, sequence.run_and_handle_errors, [tool, options, state], expected_exception=TryAgain, expected_stderr=expected_stderr) + + self.assertEquals(options.update, True) + self.assertEquals(options.build, False) + self.assertEquals(options.test, False) + + def test_manual_reject_during_processing(self): + queue = SecondThoughtsCommitQueue(MockTool()) + queue.begin_work_queue() + queue._tool.filesystem.write_text_file('/mock-results/results.html', '') # Otherwise the commit-queue will hit a KeyError trying to read the results from the MockFileSystem. + queue._options = Mock() + queue._options.port = None + expected_stderr = """MOCK: update_status: commit-queue Cleaned working directory +MOCK: update_status: commit-queue Updated working directory +MOCK: update_status: commit-queue Applied patch +MOCK: update_status: commit-queue ChangeLog validated +MOCK: update_status: commit-queue Built patch +MOCK: update_status: commit-queue Passed tests +MOCK: update_status: commit-queue Retry +MOCK: release_work_item: commit-queue 10000 +""" + OutputCapture().assert_outputs(self, queue.process_work_item, [QueuesTest.mock_work_item], expected_stderr=expected_stderr) + + def test_report_flaky_tests(self): + queue = TestCommitQueue(MockTool()) + expected_stderr = """MOCK bug comment: bug_id=50002, cc=None +--- Begin comment --- +The commit-queue just saw foo/bar.html flake (Text diff mismatch) while processing attachment 10000 on bug 50000. +Port: MockPort Platform: MockPlatform 1.0 +--- End comment --- + +MOCK add_attachment_to_bug: bug_id=50002, description=Failure diff from bot filename=failure.diff +MOCK bug comment: bug_id=50002, cc=None +--- Begin comment --- +The commit-queue just saw bar/baz.html flake (Text diff mismatch) while processing attachment 10000 on bug 50000. +Port: MockPort Platform: MockPlatform 1.0 +--- End comment --- + +MOCK add_attachment_to_bug: bug_id=50002, description=Archive of layout-test-results from bot filename=layout-test-results.zip +MOCK bug comment: bug_id=50000, cc=None +--- Begin comment --- +The commit-queue encountered the following flaky tests while processing attachment 10000: + +foo/bar.html bug 50002 (author: abarth@webkit.org) +bar/baz.html bug 50002 (author: abarth@webkit.org) +The commit-queue is continuing to process your patch. +--- End comment --- + +""" + test_names = ["foo/bar.html", "bar/baz.html"] + test_results = [self._mock_test_result(name) for name in test_names] + + class MockZipFile(object): + def __init__(self): + self.fp = StringIO() + + def read(self, path): + return "" + + def namelist(self): + # This is intentionally missing one diffs.txt to exercise the "upload the whole zip" codepath. + return ['foo/bar-diffs.txt'] + + OutputCapture().assert_outputs(self, queue.report_flaky_tests, [QueuesTest.mock_work_item, test_results, MockZipFile()], expected_stderr=expected_stderr) + + +class StyleQueueTest(QueuesTest): + def test_style_queue_with_style_exception(self): + expected_stderr = { + "begin_work_queue": self._default_begin_work_queue_stderr("style-queue"), + "next_work_item": "", + "should_proceed_with_work_item": "MOCK: update_status: style-queue Checking style\n", + "process_work_item": """MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'check-style', '--force-clean', '--non-interactive', '--parent-command=style-queue', 10000], cwd=/mock-checkout +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'apply-watchlist-local', 50000], cwd=/mock-checkout +MOCK: update_status: style-queue Fail +MOCK: release_work_item: style-queue 10000\n""", + "handle_unexpected_error": "Mock error message\n", + "handle_script_error": "MOCK: update_status: style-queue ScriptError error message\nMOCK bug comment: bug_id=50000, cc=[]\n--- Begin comment ---\nAttachment 10000 did not pass style-queue:\n\nScriptError error message\n\nIf any of these errors are false positives, please file a bug against check-webkit-style.\n--- End comment ---\n\n", + } + expected_exceptions = { + "process_work_item": ScriptError, + "handle_script_error": SystemExit, + } + tool = MockTool(log_executive=True, executive_throws_when_run=set(['check-style'])) + self.assert_queue_outputs(StyleQueue(), expected_stderr=expected_stderr, expected_exceptions=expected_exceptions, tool=tool) + + def test_style_queue_with_watch_list_exception(self): + expected_stderr = { + "begin_work_queue": self._default_begin_work_queue_stderr("style-queue"), + "next_work_item": "", + "should_proceed_with_work_item": "MOCK: update_status: style-queue Checking style\n", + "process_work_item": """MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'check-style', '--force-clean', '--non-interactive', '--parent-command=style-queue', 10000], cwd=/mock-checkout +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'apply-watchlist-local', 50000], cwd=/mock-checkout +MOCK: update_status: style-queue Pass +MOCK: release_work_item: style-queue 10000\n""", + "handle_unexpected_error": "Mock error message\n", + "handle_script_error": "MOCK: update_status: style-queue ScriptError error message\nMOCK bug comment: bug_id=50000, cc=[]\n--- Begin comment ---\nAttachment 10000 did not pass style-queue:\n\nScriptError error message\n\nIf any of these errors are false positives, please file a bug against check-webkit-style.\n--- End comment ---\n\n", + } + expected_exceptions = { + "handle_script_error": SystemExit, + } + tool = MockTool(log_executive=True, executive_throws_when_run=set(['apply-watchlist-local'])) + self.assert_queue_outputs(StyleQueue(), expected_stderr=expected_stderr, expected_exceptions=expected_exceptions, tool=tool) diff --git a/Tools/Scripts/webkitpy/tool/commands/queuestest.py b/Tools/Scripts/webkitpy/tool/commands/queuestest.py new file mode 100644 index 000000000..cb16b530f --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/queuestest.py @@ -0,0 +1,99 @@ +# Copyright (C) 2009 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.bugzilla import Attachment +from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.common.system.executive import ScriptError +from webkitpy.thirdparty.mock import Mock +from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler +from webkitpy.tool.mocktool import MockTool + + +class MockQueueEngine(object): + def __init__(self, name, queue, wakeup_event): + pass + + def run(self): + pass + + +class QueuesTest(unittest.TestCase): + # This is _patch1 in mocktool.py + mock_work_item = MockTool().bugs.fetch_attachment(10000) + + def assert_outputs(self, func, func_name, args, expected_stdout, expected_stderr, expected_exceptions): + exception = None + if expected_exceptions and func_name in expected_exceptions: + exception = expected_exceptions[func_name] + + OutputCapture().assert_outputs(self, + func, + args=args, + expected_stdout=expected_stdout.get(func_name, ""), + expected_stderr=expected_stderr.get(func_name, ""), + expected_exception=exception) + + def _default_begin_work_queue_stderr(self, name): + checkout_dir = '/mock-checkout' + string_replacements = {"name": name, 'checkout_dir': checkout_dir} + return "CAUTION: %(name)s will discard all local changes in \"%(checkout_dir)s\"\nRunning WebKit %(name)s.\nMOCK: update_status: %(name)s Starting Queue\n" % string_replacements + + def assert_queue_outputs(self, queue, args=None, work_item=None, expected_stdout=None, expected_stderr=None, expected_exceptions=None, options=None, tool=None): + if not tool: + tool = MockTool() + # This is a hack to make it easy for callers to not have to setup a custom MockFileSystem just to test the commit-queue + # the cq tries to read the layout test results, and will hit a KeyError in MockFileSystem if we don't do this. + tool.filesystem.write_text_file('/mock-results/results.html', "") + if not expected_stdout: + expected_stdout = {} + if not expected_stderr: + expected_stderr = {} + if not args: + args = [] + if not options: + options = Mock() + options.port = None + if not work_item: + work_item = self.mock_work_item + tool.user.prompt = lambda message: "yes" + + queue.execute(options, args, tool, engine=MockQueueEngine) + + self.assert_outputs(queue.queue_log_path, "queue_log_path", [], expected_stdout, expected_stderr, expected_exceptions) + self.assert_outputs(queue.work_item_log_path, "work_item_log_path", [work_item], expected_stdout, expected_stderr, expected_exceptions) + self.assert_outputs(queue.begin_work_queue, "begin_work_queue", [], expected_stdout, expected_stderr, expected_exceptions) + self.assert_outputs(queue.should_continue_work_queue, "should_continue_work_queue", [], expected_stdout, expected_stderr, expected_exceptions) + self.assert_outputs(queue.next_work_item, "next_work_item", [], expected_stdout, expected_stderr, expected_exceptions) + self.assert_outputs(queue.should_proceed_with_work_item, "should_proceed_with_work_item", [work_item], expected_stdout, expected_stderr, expected_exceptions) + self.assert_outputs(queue.process_work_item, "process_work_item", [work_item], expected_stdout, expected_stderr, expected_exceptions) + self.assert_outputs(queue.handle_unexpected_error, "handle_unexpected_error", [work_item, "Mock error message"], expected_stdout, expected_stderr, expected_exceptions) + # Should we have a different function for testing StepSequenceErrorHandlers? + if isinstance(queue, StepSequenceErrorHandler): + self.assert_outputs(queue.handle_script_error, "handle_script_error", [tool, {"patch": self.mock_work_item}, ScriptError(message="ScriptError error message", script_args="MockErrorCommand")], expected_stdout, expected_stderr, expected_exceptions) diff --git a/Tools/Scripts/webkitpy/tool/commands/rebaseline.py b/Tools/Scripts/webkitpy/tool/commands/rebaseline.py new file mode 100644 index 000000000..515ff7dfa --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/rebaseline.py @@ -0,0 +1,241 @@ +# 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 os.path +import re +import shutil +import urllib + +import webkitpy.common.config.urls as config_urls +from webkitpy.common.checkout.baselineoptimizer import BaselineOptimizer +from webkitpy.common.net.buildbot import BuildBot +from webkitpy.common.net.layouttestresults import LayoutTestResults +from webkitpy.common.system.executive import ScriptError +from webkitpy.common.system.user import User +from webkitpy.layout_tests.controllers.test_result_writer import TestResultWriter +from webkitpy.layout_tests.models import test_failures +from webkitpy.layout_tests.models.test_expectations import TestExpectations +from webkitpy.layout_tests.port import builders +from webkitpy.tool.grammar import pluralize +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand + + +_baseline_suffix_list = ['png', 'txt'] + + +# FIXME: Should TestResultWriter know how to compute this string? +def _baseline_name(fs, test_name, suffix): + return fs.splitext(test_name)[0] + TestResultWriter.FILENAME_SUFFIX_EXPECTED + "." + suffix + + +class RebaselineTest(AbstractDeclarativeCommand): + name = "rebaseline-test" + help_text = "Rebaseline a single test from a buildbot. (Currently works only with build.chromium.org buildbots.)" + argument_names = "BUILDER_NAME TEST_NAME" + + def _results_url(self, builder_name): + # FIXME: Generalize this command to work with non-build.chromium.org builders. + builder = self._tool.chromium_buildbot().builder_with_name(builder_name) + return builder.accumulated_results_url() + + def _baseline_directory(self, builder_name): + port = self._tool.port_factory.get_from_builder_name(builder_name) + return port.baseline_path() + + def _save_baseline(self, data, target_baseline): + if not data: + return + filesystem = self._tool.filesystem + filesystem.maybe_make_directory(filesystem.dirname(target_baseline)) + filesystem.write_binary_file(target_baseline, data) + if not self._tool.scm().exists(target_baseline): + self._tool.scm().add(target_baseline) + + def _test_root(self, test_name): + return os.path.splitext(test_name)[0] + + def _file_name_for_actual_result(self, test_name, suffix): + return "%s-actual.%s" % (self._test_root(test_name), suffix) + + def _file_name_for_expected_result(self, test_name, suffix): + return "%s-expected.%s" % (self._test_root(test_name), suffix) + + def _rebaseline_test(self, builder_name, test_name, suffix): + results_url = self._results_url(builder_name) + baseline_directory = self._baseline_directory(builder_name) + + source_baseline = "%s/%s" % (results_url, self._file_name_for_actual_result(test_name, suffix)) + target_baseline = os.path.join(baseline_directory, self._file_name_for_expected_result(test_name, suffix)) + + print "Retrieving %s." % source_baseline + self._save_baseline(self._tool.web.get_binary(source_baseline, convert_404_to_None=True), target_baseline) + + def execute(self, options, args, tool): + for suffix in _baseline_suffix_list: + self._rebaseline_test(args[0], args[1], suffix) + + +class OptimizeBaselines(AbstractDeclarativeCommand): + name = "optimize-baselines" + help_text = "Reshuffles the baselines for the given tests to use as litte space on disk as possible." + argument_names = "TEST_NAMES" + + def _optimize_baseline(self, test_name): + for suffix in _baseline_suffix_list: + baseline_name = _baseline_name(self._tool.filesystem, test_name, suffix) + if not self._baseline_optimizer.optimize(baseline_name): + print "Hueristics failed to optimize %s" % baseline_name + + def execute(self, options, args, tool): + self._baseline_optimizer = BaselineOptimizer(tool) + self._port = tool.port_factory.get("chromium-win-win7") # FIXME: This should be selectable. + + for test_name in self._port.tests(args): + print "Optimizing %s." % test_name + self._optimize_baseline(test_name) + + +class AnalyzeBaselines(AbstractDeclarativeCommand): + name = "analyze-baselines" + help_text = "Analyzes the baselines for the given tests and prints results that are identical." + argument_names = "TEST_NAMES" + + def _print(self, baseline_name, directories_by_result): + for result, directories in directories_by_result.items(): + if len(directories) <= 1: + continue + results_names = [self._tool.filesystem.join(directory, baseline_name) for directory in directories] + print ' '.join(results_names) + + def _analyze_baseline(self, test_name): + for suffix in _baseline_suffix_list: + baseline_name = _baseline_name(self._tool.filesystem, test_name, suffix) + directories_by_result = self._baseline_optimizer.directories_by_result(baseline_name) + self._print(baseline_name, directories_by_result) + + def execute(self, options, args, tool): + self._baseline_optimizer = BaselineOptimizer(tool) + self._port = tool.port_factory.get("chromium-win-win7") # FIXME: This should be selectable. + + for test_name in self._port.tests(args): + self._analyze_baseline(test_name) + + +class RebaselineExpectations(AbstractDeclarativeCommand): + name = "rebaseline-expectations" + help_text = "Rebaselines the tests indicated in test_expectations.txt." + + def _run_webkit_patch(self, args): + try: + self._tool.executive.run_command([self._tool.path()] + args, cwd=self._tool.scm().checkout_root) + except ScriptError, e: + pass + + def _is_supported_port(self, port_name): + # FIXME: Support non-Chromium ports. + return port_name.startswith('chromium-') + + def _expectations(self, port): + return TestExpectations(port, None, port.test_expectations(), port.test_configuration()) + + def _update_expectations_file(self, port_name): + if not self._is_supported_port(port_name): + return + port = self._tool.port_factory.get(port_name) + expectations = self._expectations(port) + path = port.path_to_test_expectations_file() + self._tool.filesystem.write_text_file(path, expectations.remove_rebaselined_tests(expectations.get_rebaselining_failures())) + + def _tests_to_rebaseline(self, port): + return self._expectations(port).get_rebaselining_failures() + + def _rebaseline_port(self, port_name): + if not self._is_supported_port(port_name): + return + builder_name = builders.builder_name_for_port_name(port_name) + if not builder_name: + return + print "Retrieving results for %s from %s." % (port_name, builder_name) + for test_name in self._tests_to_rebaseline(self._tool.port_factory.get(port_name)): + self._touched_test_names.add(test_name) + print " %s" % test_name + self._run_webkit_patch(['rebaseline-test', builder_name, test_name]) + + def execute(self, options, args, tool): + self._touched_test_names = set([]) + for port_name in tool.port_factory.all_port_names(): + self._rebaseline_port(port_name) + for port_name in tool.port_factory.all_port_names(): + self._update_expectations_file(port_name) + for test_name in self._touched_test_names: + print "Optimizing baselines for %s." % test_name + self._run_webkit_patch(['optimize-baselines', test_name]) + + +class Rebaseline(AbstractDeclarativeCommand): + name = "rebaseline" + help_text = "Replaces local expected.txt files with new results from build bots" + + # FIXME: This should share more code with FailureReason._builder_to_explain + def _builder_to_pull_from(self): + builder_statuses = self._tool.buildbot.builder_statuses() + red_statuses = [status for status in builder_statuses if not status["is_green"]] + print "%s failing" % (pluralize("builder", len(red_statuses))) + builder_choices = [status["name"] for status in red_statuses] + chosen_name = self._tool.user.prompt_with_list("Which builder to pull results from:", builder_choices) + # FIXME: prompt_with_list should really take a set of objects and a set of names and then return the object. + for status in red_statuses: + if status["name"] == chosen_name: + return (self._tool.buildbot.builder_with_name(chosen_name), status["build_number"]) + + def _replace_expectation_with_remote_result(self, local_file, remote_file): + (downloaded_file, headers) = urllib.urlretrieve(remote_file) + shutil.move(downloaded_file, local_file) + + def _tests_to_update(self, build): + failing_tests = build.layout_test_results().tests_matching_failure_types([test_failures.FailureTextMismatch]) + return self._tool.user.prompt_with_list("Which test(s) to rebaseline:", failing_tests, can_choose_multiple=True) + + def _results_url_for_test(self, build, test): + test_base = os.path.splitext(test)[0] + actual_path = test_base + "-actual.txt" + return build.results_url() + "/" + actual_path + + def execute(self, options, args, tool): + builder, build_number = self._builder_to_pull_from() + build = builder.build(build_number) + port = tool.port_factory.get_from_builder_name(builder.name()) + + for test in self._tests_to_update(build): + results_url = self._results_url_for_test(build, test) + # Port operates with absolute paths. + expected_file = port.expected_filename(test, '.txt') + print test + self._replace_expectation_with_remote_result(expected_file, results_url) + + # FIXME: We should handle new results too. diff --git a/Tools/Scripts/webkitpy/tool/commands/rebaseline_unittest.py b/Tools/Scripts/webkitpy/tool/commands/rebaseline_unittest.py new file mode 100644 index 000000000..bcb0921aa --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/rebaseline_unittest.py @@ -0,0 +1,120 @@ +# 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.system.outputcapture import OutputCapture +from webkitpy.thirdparty.mock import Mock +from webkitpy.tool.commands.rebaseline import * +from webkitpy.tool.mocktool import MockTool +from webkitpy.common.system.executive_mock import MockExecutive + + +class TestRebaseline(unittest.TestCase): + def test_tests_to_update(self): + command = Rebaseline() + command.bind_to_tool(MockTool()) + build = Mock() + OutputCapture().assert_outputs(self, command._tests_to_update, [build]) + + def test_rebaseline_test(self): + command = RebaselineTest() + command.bind_to_tool(MockTool()) + expected_stdout = "Retrieving http://example.com/f/builders/Webkit Linux/results/layout-test-results/userscripts/another-test-actual.txt.\n" + OutputCapture().assert_outputs(self, command._rebaseline_test, ["Webkit Linux", "userscripts/another-test.html", "txt"], expected_stdout=expected_stdout) + + def test_rebaseline_expectations(self): + command = RebaselineExpectations() + tool = MockTool() + command.bind_to_tool(tool) + + for port_name in tool.port_factory.all_port_names(): + port = tool.port_factory.get(port_name) + tool.filesystem.write_text_file(port.path_to_test_expectations_file(), '') + + # Don't enable logging until after we create the mock expectation files as some Port.__init__'s run subcommands. + tool.executive = MockExecutive(should_log=True) + + expected_stdout = """Retrieving results for chromium-cg-mac-leopard from Webkit Mac10.5 (CG). + userscripts/another-test.html + userscripts/images.svg +Retrieving results for chromium-cg-mac-snowleopard from Webkit Mac10.6 (CG). + userscripts/another-test.html + userscripts/images.svg +Retrieving results for chromium-gpu-cg-mac-leopard from Webkit Mac10.5 (CG) - GPU. +Retrieving results for chromium-gpu-cg-mac-snowleopard from Webkit Mac10.6 (CG) - GPU. +Retrieving results for chromium-gpu-mac-snowleopard from Webkit Mac10.6 - GPU. +Retrieving results for chromium-gpu-win-win7 from Webkit Win7 - GPU. +Retrieving results for chromium-gpu-win-xp from Webkit Win - GPU. +Retrieving results for chromium-linux-x86 from Webkit Linux 32. + userscripts/another-test.html + userscripts/images.svg +Retrieving results for chromium-linux-x86_64 from Webkit Linux. + userscripts/another-test.html + userscripts/images.svg +Retrieving results for chromium-mac-leopard from Webkit Mac10.5. + userscripts/another-test.html + userscripts/images.svg +Retrieving results for chromium-mac-snowleopard from Webkit Mac10.6. + userscripts/another-test.html + userscripts/images.svg +Retrieving results for chromium-win-vista from Webkit Vista. + userscripts/another-test.html + userscripts/images.svg +Retrieving results for chromium-win-win7 from Webkit Win7. + userscripts/another-test.html + userscripts/images.svg +Retrieving results for chromium-win-xp from Webkit Win. + userscripts/another-test.html + userscripts/images.svg +Optimizing baselines for userscripts/another-test.html. +Optimizing baselines for userscripts/images.svg. +""" + expected_stderr = """MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.5 (CG)', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.5 (CG)', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.6 (CG)', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.6 (CG)', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Linux 32', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Linux 32', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Linux', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Linux', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.5', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.5', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.6', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.6', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Vista', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Vista', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Win7', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Win7', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Win', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Win', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'optimize-baselines', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'optimize-baselines', 'userscripts/images.svg'], cwd=/mock-checkout +""" + command._tests_to_rebaseline = lambda port: [] if not port.name().find('-gpu-') == -1 else ['userscripts/another-test.html', 'userscripts/images.svg'] + OutputCapture().assert_outputs(self, command.execute, [None, [], tool], expected_stdout=expected_stdout, expected_stderr=expected_stderr) diff --git a/Tools/Scripts/webkitpy/tool/commands/rebaselineserver.py b/Tools/Scripts/webkitpy/tool/commands/rebaselineserver.py new file mode 100644 index 000000000..e7ff86a14 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/rebaselineserver.py @@ -0,0 +1,101 @@ +# 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. + +"""Starts a local HTTP server which displays layout test failures (given a test +results directory), provides comparisons of expected and actual results (both +images and text) and allows one-click rebaselining of tests.""" + +import os +import os.path + +from webkitpy.common import system +from webkitpy.common.net.resultsjsonparser import for_each_test, JSONTestResult +from webkitpy.layout_tests.layout_package import json_results_generator +from webkitpy.tool.commands.abstractlocalservercommand import AbstractLocalServerCommand +from webkitpy.tool.servers.rebaselineserver import get_test_baselines, RebaselineHTTPServer, STATE_NEEDS_REBASELINE + + +class TestConfig(object): + def __init__(self, test_port, layout_tests_directory, results_directory, platforms, filesystem, scm): + self.test_port = test_port + self.layout_tests_directory = layout_tests_directory + self.results_directory = results_directory + self.platforms = platforms + self.filesystem = filesystem + self.scm = scm + + +class RebaselineServer(AbstractLocalServerCommand): + name = "rebaseline-server" + help_text = __doc__ + argument_names = "/path/to/results/directory" + + server = RebaselineHTTPServer + + def _gather_baselines(self, results_json): + # Rebaseline server and it's associated JavaScript expected the tests subtree to + # be key-value pairs instead of hierarchical. + # FIXME: make the rebaseline server use the hierarchical tree. + new_tests_subtree = {} + + def gather_baselines_for_test(test_name, result_dict): + result = JSONTestResult(test_name, result_dict) + if result.did_pass_or_run_as_expected(): + return + result_dict['state'] = STATE_NEEDS_REBASELINE + result_dict['baselines'] = get_test_baselines(test_name, self._test_config) + new_tests_subtree[test_name] = result_dict + + for_each_test(results_json['tests'], gather_baselines_for_test) + results_json['tests'] = new_tests_subtree + + def _prepare_config(self, options, args, tool): + results_directory = args[0] + filesystem = system.filesystem.FileSystem() + scm = self._tool.scm() + + print 'Parsing full_results.json...' + results_json_path = filesystem.join(results_directory, 'full_results.json') + results_json = json_results_generator.load_json(filesystem, results_json_path) + + port = tool.port_factory.get() + layout_tests_directory = port.layout_tests_dir() + platforms = filesystem.listdir(filesystem.join(layout_tests_directory, 'platform')) + self._test_config = TestConfig(port, layout_tests_directory, results_directory, platforms, filesystem, scm) + + print 'Gathering current baselines...' + self._gather_baselines(results_json) + + return { + 'test_config': self._test_config, + "results_json": results_json, + "platforms_json": { + 'platforms': platforms, + 'defaultPlatform': port.name(), + }, + } diff --git a/Tools/Scripts/webkitpy/tool/commands/roll.py b/Tools/Scripts/webkitpy/tool/commands/roll.py new file mode 100644 index 000000000..37481b2b8 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/roll.py @@ -0,0 +1,74 @@ +# Copyright (c) 2011 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. + +from webkitpy.tool.commands.abstractsequencedcommand import AbstractSequencedCommand + +from webkitpy.tool import steps + + +class RollChromiumDEPS(AbstractSequencedCommand): + name = "roll-chromium-deps" + help_text = "Updates Chromium DEPS (defaults to the last-known good revision of Chromium)" + argument_names = "[CHROMIUM_REVISION]" + steps = [ + steps.UpdateChromiumDEPS, + steps.PrepareChangeLogForDEPSRoll, + steps.ConfirmDiff, + steps.Commit, + ] + + def _prepare_state(self, options, args, tool): + return { + "chromium_revision": (args and args[0]), + } + + +class PostChromiumDEPSRoll(AbstractSequencedCommand): + name = "post-chromium-deps-roll" + help_text = "Posts a patch to update Chromium DEPS (revision defaults to the last-known good revision of Chromium)" + argument_names = "CHROMIUM_REVISION CHROMIUM_REVISION_NAME" + steps = [ + steps.CleanWorkingDirectory, + steps.Update, + steps.UpdateChromiumDEPS, + steps.PrepareChangeLogForDEPSRoll, + steps.CreateBug, + steps.PostDiff, + ] + + def _prepare_state(self, options, args, tool): + options.review = False + options.request_commit = True + + chromium_revision = args[0] + chromium_revision_name = args[1] + return { + "chromium_revision": chromium_revision, + "bug_title": "Roll Chromium DEPS to %s" % chromium_revision_name, + "bug_description": "A DEPS roll a day keeps the build break away.", + } diff --git a/Tools/Scripts/webkitpy/tool/commands/roll_unittest.py b/Tools/Scripts/webkitpy/tool/commands/roll_unittest.py new file mode 100644 index 000000000..800bc5b96 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/roll_unittest.py @@ -0,0 +1,63 @@ +# Copyright (C) 2011 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. + +from webkitpy.thirdparty.mock import Mock +from webkitpy.tool.commands.commandtest import CommandsTest +from webkitpy.tool.commands.roll import * +from webkitpy.tool.mocktool import MockOptions, MockTool + + +class RollCommandsTest(CommandsTest): + def test_update_chromium_deps(self): + expected_stderr = """Updating Chromium DEPS to 6764 +MOCK: MockDEPS.write_variable(chromium_rev, 6764) +MOCK: user.open_url: file://... +Was that diff correct? +Committed r49824: <http://trac.webkit.org/changeset/49824> +""" + self.assert_execute_outputs(RollChromiumDEPS(), [6764], expected_stderr=expected_stderr) + + def test_update_chromium_deps_older_revision(self): + options = MockOptions(non_interactive=False) + expected_stderr = """Current Chromium DEPS revision 6564 is newer than 5764. +ERROR: Unable to update Chromium DEPS +""" + self.assert_execute_outputs(RollChromiumDEPS(), [5764], options=options, expected_stderr=expected_stderr, expected_exception=SystemExit) + + +class PostRollCommandsTest(CommandsTest): + def test_prepare_state(self): + postroll = PostChromiumDEPSRoll() + options = MockOptions() + tool = MockTool() + lkgr_state = postroll._prepare_state(options, [None, "last-known good revision"], tool) + self.assertEquals(None, lkgr_state["chromium_revision"]) + self.assertEquals("Roll Chromium DEPS to last-known good revision", lkgr_state["bug_title"]) + revision_state = postroll._prepare_state(options, ["1234", "r1234"], tool) + self.assertEquals("1234", revision_state["chromium_revision"]) + self.assertEquals("Roll Chromium DEPS to r1234", revision_state["bug_title"]) diff --git a/Tools/Scripts/webkitpy/tool/commands/sheriffbot.py b/Tools/Scripts/webkitpy/tool/commands/sheriffbot.py new file mode 100644 index 000000000..547309e88 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/sheriffbot.py @@ -0,0 +1,80 @@ +# Copyright (c) 2009 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 os + +from webkitpy.common.system.deprecated_logging import log +from webkitpy.common.config.ports import WebKitPort +from webkitpy.tool.bot.sheriff import Sheriff +from webkitpy.tool.bot.sheriffircbot import SheriffIRCBot +from webkitpy.tool.commands.queues import AbstractQueue +from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler + + +class SheriffBot(AbstractQueue, StepSequenceErrorHandler): + name = "sheriff-bot" + watchers = AbstractQueue.watchers + [ + "abarth@webkit.org", + "eric@webkit.org", + ] + + # AbstractQueue methods + + def begin_work_queue(self): + AbstractQueue.begin_work_queue(self) + self._sheriff = Sheriff(self._tool, self) + self._irc_bot = SheriffIRCBot(self._tool, self._sheriff) + self._tool.ensure_irc_connected(self._irc_bot.irc_delegate()) + + def work_item_log_path(self, failure_map): + return None + + def _is_old_failure(self, revision): + return self._tool.status_server.svn_revision(revision) + + def next_work_item(self): + self._irc_bot.process_pending_messages() + return + + def should_proceed_with_work_item(self, failure_map): + # Currently, we don't have any reasons not to proceed with work items. + return True + + def process_work_item(self, failure_map): + return True + + def handle_unexpected_error(self, failure_map, message): + log(message) + + # StepSequenceErrorHandler methods + + @classmethod + def handle_script_error(cls, tool, state, script_error): + # Ideally we would post some information to IRC about what went wrong + # here, but we don't have the IRC password in the child process. + pass diff --git a/Tools/Scripts/webkitpy/tool/commands/sheriffbot_unittest.py b/Tools/Scripts/webkitpy/tool/commands/sheriffbot_unittest.py new file mode 100644 index 000000000..735ccab69 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/sheriffbot_unittest.py @@ -0,0 +1,37 @@ +# 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 os + +from webkitpy.tool.commands.queuestest import QueuesTest +from webkitpy.tool.commands.sheriffbot import SheriffBot +from webkitpy.tool.mocktool import * + + +class SheriffBotTest(QueuesTest): + pass # No unittests as the moment. diff --git a/Tools/Scripts/webkitpy/tool/commands/stepsequence.py b/Tools/Scripts/webkitpy/tool/commands/stepsequence.py new file mode 100644 index 000000000..b66655446 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/stepsequence.py @@ -0,0 +1,83 @@ +# Copyright (C) 2009 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. + +from webkitpy.tool import steps + +from webkitpy.common.checkout.scm import CheckoutNeedsUpdate +from webkitpy.common.system.deprecated_logging import log +from webkitpy.common.system.executive import ScriptError +from webkitpy.tool.bot.queueengine import QueueEngine + + +class StepSequenceErrorHandler(): + @classmethod + def handle_script_error(cls, tool, patch, script_error): + raise NotImplementedError, "subclasses must implement" + + @classmethod + def handle_checkout_needs_update(cls, tool, state, options, error): + raise NotImplementedError, "subclasses must implement" + + +class StepSequence(object): + def __init__(self, steps): + self._steps = steps or [] + + def options(self): + collected_options = [ + steps.Options.parent_command, + steps.Options.quiet, + ] + for step in self._steps: + collected_options = collected_options + step.options() + # Remove duplicates. + collected_options = sorted(set(collected_options)) + return collected_options + + def _run(self, tool, options, state): + for step in self._steps: + step(tool, options).run(state) + + def run_and_handle_errors(self, tool, options, state=None): + if not state: + state = {} + try: + self._run(tool, options, state) + except CheckoutNeedsUpdate, e: + log("Commit failed because the checkout is out of date. Please update and try again.") + if options.parent_command: + command = tool.command_by_name(options.parent_command) + command.handle_checkout_needs_update(tool, state, options, e) + QueueEngine.exit_after_handled_error(e) + except ScriptError, e: + if not options.quiet: + log(e.message_with_output()) + if options.parent_command: + command = tool.command_by_name(options.parent_command) + command.handle_script_error(tool, state, e) + QueueEngine.exit_after_handled_error(e) diff --git a/Tools/Scripts/webkitpy/tool/commands/suggestnominations.py b/Tools/Scripts/webkitpy/tool/commands/suggestnominations.py new file mode 100644 index 000000000..c197a1116 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/suggestnominations.py @@ -0,0 +1,247 @@ +# Copyright (c) 2011 Google Inc. All rights reserved. +# Copyright (c) 2011 Code Aurora Forum. 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. + +from optparse import make_option +import re + +from webkitpy.common.checkout.changelog import ChangeLogEntry +from webkitpy.common.config.committers import CommitterList +from webkitpy.tool import steps +from webkitpy.tool.grammar import join_with_separators +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand + + +class SuggestNominations(AbstractDeclarativeCommand): + name = "suggest-nominations" + help_text = "Suggest contributors for committer/reviewer nominations" + + def __init__(self): + options = [ + make_option("--committer-minimum", action="store", dest="committer_minimum", type="int", default=10, help="Specify minimum patch count for Committer nominations."), + make_option("--reviewer-minimum", action="store", dest="reviewer_minimum", type="int", default=80, help="Specify minimum patch count for Reviewer nominations."), + make_option("--max-commit-age", action="store", dest="max_commit_age", type="int", default=9, help="Specify max commit age to consider for nominations (in months)."), + make_option("--show-commits", action="store_true", dest="show_commits", default=False, help="Show commit history with nomination suggestions."), + ] + + AbstractDeclarativeCommand.__init__(self, options=options) + # FIXME: This should probably be on the tool somewhere. + self._committer_list = CommitterList() + + _counters_by_name = {} + _counters_by_email = {} + + def _init_options(self, options): + self.committer_minimum = options.committer_minimum + self.reviewer_minimum = options.reviewer_minimum + self.max_commit_age = options.max_commit_age + self.show_commits = options.show_commits + self.verbose = options.verbose + + # FIXME: This should move to scm.py + def _recent_commit_messages(self): + git_log = self._tool.executive.run_command(['git', 'log', '--since="%s months ago"' % self.max_commit_age]) + match_git_svn_id = re.compile(r"\n\n git-svn-id:.*\n", re.MULTILINE) + match_get_log_lines = re.compile(r"^\S.*\n", re.MULTILINE) + match_leading_indent = re.compile(r"^[ ]{4}", re.MULTILINE) + + messages = re.split(r"commit \w{40}", git_log)[1:] # Ignore the first message which will be empty. + for message in messages: + # Remove any lines from git and unindent all the lines + (message, _) = match_git_svn_id.subn("", message) + (message, _) = match_get_log_lines.subn("", message) + (message, _) = match_leading_indent.subn("", message) + yield message.lstrip() # Remove any leading newlines from the log message. + + # e.g. Patch by Eric Seidel <eric@webkit.org> on 2011-09-15 + patch_by_regexp = r'^Patch by (?P<name>.+?)\s+<(?P<email>[^<>]+)> on (?P<date>\d{4}-\d{2}-\d{2})$' + + def _count_recent_patches(self): + # This entire block could be written as a map/reduce over the messages. + for message in self._recent_commit_messages(): + # FIXME: This should use ChangeLogEntry to do the entire parse instead + # of grabbing at its regexps. + dateline_match = re.match(ChangeLogEntry.date_line_regexp, message, re.MULTILINE) + if not dateline_match: + # Modern commit messages don't just dump the ChangeLog entry, but rather + # have a special Patch by line for non-committers. + dateline_match = re.search(self.patch_by_regexp, message, re.MULTILINE) + if not dateline_match: + continue + + author_email = dateline_match.group("email") + if not author_email: + continue + + # We only care about reviewed patches, so make sure it has a valid reviewer line. + reviewer_match = re.search(ChangeLogEntry.reviewed_by_regexp, message, re.MULTILINE) + # We might also want to validate the reviewer name against the committer list. + if not reviewer_match or not reviewer_match.group("reviewer"): + continue + + author_name = dateline_match.group("name") + if not author_name: + continue + + if re.search("([^a-zA-Z]and[^a-zA-Z])|(,)|(@)", author_name): + # This entry seems to have multiple reviewers, or invalid characters, so reject it. + continue + + svn_id_match = re.search(ChangeLogEntry.svn_id_regexp, message, re.MULTILINE) + if svn_id_match: + svn_id = svn_id_match.group("svnid") + if not svn_id_match or not svn_id: + svn_id = "unknown" + commit_date = dateline_match.group("date") + + # See if we already have a contributor with this name or email + counter_by_name = self._counters_by_name.get(author_name) + counter_by_email = self._counters_by_email.get(author_email) + if counter_by_name: + if counter_by_email: + if counter_by_name != counter_by_email: + # Merge these two counters This is for the case where we had + # John Smith (jsmith@gmail.com) and Jonathan Smith (jsmith@apple.com) + # and just found a John Smith (jsmith@apple.com). Now we know the + # two names are the same person + counter_by_name['names'] |= counter_by_email['names'] + counter_by_name['emails'] |= counter_by_email['emails'] + counter_by_name['count'] += counter_by_email.get('count', 0) + self._counters_by_email[author_email] = counter_by_name + else: + # Add email to the existing counter + self._counters_by_email[author_email] = counter_by_name + counter_by_name['emails'] |= set([author_email]) + else: + if counter_by_email: + # Add name to the existing counter + self._counters_by_name[author_name] = counter_by_email + counter_by_email['names'] |= set([author_name]) + else: + # Create new counter + new_counter = {'names': set([author_name]), 'emails': set([author_email]), 'latest_name': author_name, 'latest_email': author_email, 'commits': ""} + self._counters_by_name[author_name] = new_counter + self._counters_by_email[author_email] = new_counter + + assert(self._counters_by_name[author_name] == self._counters_by_email[author_email]) + counter = self._counters_by_name[author_name] + counter['count'] = counter.get('count', 0) + 1 + + if svn_id.isdigit(): + svn_id = "http://trac.webkit.org/changeset/" + svn_id + counter['commits'] += " commit: %s on %s by %s (%s)\n" % (svn_id, commit_date, author_name, author_email) + + return self._counters_by_email + + def _collect_nominations(self, counters_by_email): + nominations = [] + for author_email, counter in counters_by_email.items(): + if author_email != counter['latest_email']: + continue + roles = [] + + contributor = self._committer_list.contributor_by_email(author_email) + + author_name = counter['latest_name'] + patch_count = counter['count'] + + if patch_count >= self.committer_minimum and (not contributor or not contributor.can_commit): + roles.append("committer") + if patch_count >= self.reviewer_minimum and (not contributor or not contributor.can_review): + roles.append("reviewer") + if roles: + nominations.append({ + 'roles': roles, + 'author_name': author_name, + 'author_email': author_email, + 'patch_count': patch_count, + }) + return nominations + + def _print_nominations(self, nominations): + def nomination_cmp(a_nomination, b_nomination): + roles_result = cmp(a_nomination['roles'], b_nomination['roles']) + if roles_result: + return -roles_result + count_result = cmp(a_nomination['patch_count'], b_nomination['patch_count']) + if count_result: + return -count_result + return cmp(a_nomination['author_name'], b_nomination['author_name']) + + for nomination in sorted(nominations, nomination_cmp): + # This is a little bit of a hack, but its convienent to just pass the nomination dictionary to the formating operator. + nomination['roles_string'] = join_with_separators(nomination['roles']).upper() + print "%(roles_string)s: %(author_name)s (%(author_email)s) has %(patch_count)s reviewed patches" % nomination + counter = self._counters_by_email[nomination['author_email']] + + if self.show_commits: + print counter['commits'] + + def _print_counts(self, counters_by_email): + def counter_cmp(a_tuple, b_tuple): + # split the tuples + # the second element is the "counter" structure + _, a_counter = a_tuple + _, b_counter = b_tuple + + count_result = cmp(a_counter['count'], b_counter['count']) + if count_result: + return -count_result + return cmp(a_counter['latest_name'].lower(), b_counter['latest_name'].lower()) + + for author_email, counter in sorted(counters_by_email.items(), counter_cmp): + if author_email != counter['latest_email']: + continue + contributor = self._committer_list.contributor_by_email(author_email) + author_name = counter['latest_name'] + patch_count = counter['count'] + counter['names'] = counter['names'] - set([author_name]) + counter['emails'] = counter['emails'] - set([author_email]) + + alias_list = [] + for alias in counter['names']: + alias_list.append(alias) + for alias in counter['emails']: + alias_list.append(alias) + if alias_list: + print "CONTRIBUTOR: %s (%s) has %d reviewed patches %s" % (author_name, author_email, patch_count, "(aliases: " + ", ".join(alias_list) + ")") + else: + print "CONTRIBUTOR: %s (%s) has %d reviewed patches" % (author_name, author_email, patch_count) + return + + def execute(self, options, args, tool): + self._init_options(options) + patch_counts = self._count_recent_patches() + nominations = self._collect_nominations(patch_counts) + self._print_nominations(nominations) + if self.verbose: + self._print_counts(patch_counts) + + +if __name__ == "__main__": + SuggestNominations() diff --git a/Tools/Scripts/webkitpy/tool/commands/suggestnominations_unittest.py b/Tools/Scripts/webkitpy/tool/commands/suggestnominations_unittest.py new file mode 100644 index 000000000..88be25303 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/suggestnominations_unittest.py @@ -0,0 +1,77 @@ +# Copyright (C) 2011 Google Inc. All rights reserved. +# Copyright (C) 2011 Code Aurora Forum. 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. + +from webkitpy.tool.commands.commandtest import CommandsTest +from webkitpy.tool.commands.suggestnominations import SuggestNominations +from webkitpy.tool.mocktool import MockOptions, MockTool + + +class SuggestNominationsTest(CommandsTest): + + mock_git_output = """commit 60831dde5beb22f35aef305a87fca7b5f284c698 +Author: fpizlo@apple.com <fpizlo@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc> +Date: Thu Sep 15 19:56:21 2011 +0000 + + Value profiles collect no information for global variables + https://bugs.webkit.org/show_bug.cgi?id=68143 + + Reviewed by Geoffrey Garen. + + git-svn-id: http://svn.webkit.org/repository/webkit/trunk@95219 268f45cc-cd09-0410-ab3c-d52691b4dbfc +""" + mock_same_author_commit_message = """Value profiles collect no information for global variables +https://bugs.webkit.org/show_bug.cgi?id=68143 + +Reviewed by Geoffrey Garen.""" + + def test_recent_commit_messages(self): + tool = MockTool() + suggest_nominations = SuggestNominations() + suggest_nominations._init_options(options=MockOptions(reviewer_minimum=80, committer_minimum=10, max_commit_age=9, show_commits=False, verbose=False)) + suggest_nominations.bind_to_tool(tool) + + tool.executive.run_command = lambda command: self.mock_git_output + self.assertEqual(list(suggest_nominations._recent_commit_messages()), [self.mock_same_author_commit_message]) + + mock_non_committer_commit_message = """Let TestWebKitAPI work for chromium +https://bugs.webkit.org/show_bug.cgi?id=67756 + +Patch by Xianzhu Wang <wangxianzhu@chromium.org> on 2011-09-15 +Reviewed by Sam Weinig. + +Source/WebKit/chromium: + +* WebKit.gyp:""" + + def test_basic(self): + expected_stdout = "REVIEWER: Xianzhu Wang (wangxianzhu@chromium.org) has 88 reviewed patches\n" + suggest_nominations = SuggestNominations() + suggest_nominations._init_options(options=MockOptions(reviewer_minimum=80, committer_minimum=10, max_commit_age=9, show_commits=False, verbose=False)) + suggest_nominations._recent_commit_messages = lambda: [self.mock_non_committer_commit_message for _ in range(88)] + self.assert_execute_outputs(suggest_nominations, [], expected_stdout=expected_stdout, options=MockOptions(reviewer_minimum=80, committer_minimum=10, max_commit_age=9, show_commits=False, verbose=False)) diff --git a/Tools/Scripts/webkitpy/tool/commands/upload.py b/Tools/Scripts/webkitpy/tool/commands/upload.py new file mode 100644 index 000000000..1436a9378 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/upload.py @@ -0,0 +1,504 @@ +#!/usr/bin/env python +# Copyright (c) 2009, 2010 Google Inc. All rights reserved. +# Copyright (c) 2009 Apple 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 os +import re +import sys + +from optparse import make_option + +from webkitpy.tool import steps + +from webkitpy.common.checkout.changelog import parse_bug_id_from_changelog +from webkitpy.common.config.committers import CommitterList +from webkitpy.common.system.deprecated_logging import error, log +from webkitpy.common.system.user import User +from webkitpy.thirdparty.mock import Mock +from webkitpy.tool.commands.abstractsequencedcommand import AbstractSequencedCommand +from webkitpy.tool.comments import bug_comment_from_svn_revision +from webkitpy.tool.grammar import pluralize, join_with_separators +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand + + +class CommitMessageForCurrentDiff(AbstractDeclarativeCommand): + name = "commit-message" + help_text = "Print a commit message suitable for the uncommitted changes" + + def __init__(self): + options = [ + steps.Options.git_commit, + ] + AbstractDeclarativeCommand.__init__(self, options=options) + + def execute(self, options, args, tool): + # This command is a useful test to make sure commit_message_for_this_commit + # always returns the right value regardless of the current working directory. + print "%s" % tool.checkout().commit_message_for_this_commit(options.git_commit).message() + + +class CleanPendingCommit(AbstractDeclarativeCommand): + name = "clean-pending-commit" + help_text = "Clear r+ on obsolete patches so they do not appear in the pending-commit list." + + # NOTE: This was designed to be generic, but right now we're only processing patches from the pending-commit list, so only r+ matters. + def _flags_to_clear_on_patch(self, patch): + if not patch.is_obsolete(): + return None + what_was_cleared = [] + if patch.review() == "+": + if patch.reviewer(): + what_was_cleared.append("%s's review+" % patch.reviewer().full_name) + else: + what_was_cleared.append("review+") + return join_with_separators(what_was_cleared) + + def execute(self, options, args, tool): + committers = CommitterList() + for bug_id in tool.bugs.queries.fetch_bug_ids_from_pending_commit_list(): + bug = self._tool.bugs.fetch_bug(bug_id) + patches = bug.patches(include_obsolete=True) + for patch in patches: + flags_to_clear = self._flags_to_clear_on_patch(patch) + if not flags_to_clear: + continue + message = "Cleared %s from obsolete attachment %s so that this bug does not appear in http://webkit.org/pending-commit." % (flags_to_clear, patch.id()) + self._tool.bugs.obsolete_attachment(patch.id(), message) + + +# FIXME: This should be share more logic with AssignToCommitter and CleanPendingCommit +class CleanReviewQueue(AbstractDeclarativeCommand): + name = "clean-review-queue" + help_text = "Clear r? on obsolete patches so they do not appear in the pending-review list." + + def execute(self, options, args, tool): + queue_url = "http://webkit.org/pending-review" + # We do this inefficient dance to be more like webkit.org/pending-review + # bugs.queries.fetch_bug_ids_from_review_queue() doesn't return + # closed bugs, but folks using /pending-review will see them. :( + for patch_id in tool.bugs.queries.fetch_attachment_ids_from_review_queue(): + patch = self._tool.bugs.fetch_attachment(patch_id) + if not patch.review() == "?": + continue + attachment_obsolete_modifier = "" + if patch.is_obsolete(): + attachment_obsolete_modifier = "obsolete " + elif patch.bug().is_closed(): + bug_closed_explanation = " If you would like this patch reviewed, please attach it to a new bug (or re-open this bug before marking it for review again)." + else: + # Neither the patch was obsolete or the bug was closed, next patch... + continue + message = "Cleared review? from %sattachment %s so that this bug does not appear in %s.%s" % (attachment_obsolete_modifier, patch.id(), queue_url, bug_closed_explanation) + self._tool.bugs.obsolete_attachment(patch.id(), message) + + +class AssignToCommitter(AbstractDeclarativeCommand): + name = "assign-to-committer" + help_text = "Assign bug to whoever attached the most recent r+'d patch" + + def _patches_have_commiters(self, reviewed_patches): + for patch in reviewed_patches: + if not patch.committer(): + return False + return True + + def _assign_bug_to_last_patch_attacher(self, bug_id): + committers = CommitterList() + bug = self._tool.bugs.fetch_bug(bug_id) + if not bug.is_unassigned(): + assigned_to_email = bug.assigned_to_email() + log("Bug %s is already assigned to %s (%s)." % (bug_id, assigned_to_email, committers.committer_by_email(assigned_to_email))) + return + + reviewed_patches = bug.reviewed_patches() + if not reviewed_patches: + log("Bug %s has no non-obsolete patches, ignoring." % bug_id) + return + + # We only need to do anything with this bug if one of the r+'d patches does not have a valid committer (cq+ set). + if self._patches_have_commiters(reviewed_patches): + log("All reviewed patches on bug %s already have commit-queue+, ignoring." % bug_id) + return + + latest_patch = reviewed_patches[-1] + attacher_email = latest_patch.attacher_email() + committer = committers.committer_by_email(attacher_email) + if not committer: + log("Attacher %s is not a committer. Bug %s likely needs commit-queue+." % (attacher_email, bug_id)) + return + + reassign_message = "Attachment %s was posted by a committer and has review+, assigning to %s for commit." % (latest_patch.id(), committer.full_name) + self._tool.bugs.reassign_bug(bug_id, committer.bugzilla_email(), reassign_message) + + def execute(self, options, args, tool): + for bug_id in tool.bugs.queries.fetch_bug_ids_from_pending_commit_list(): + self._assign_bug_to_last_patch_attacher(bug_id) + + +class ObsoleteAttachments(AbstractSequencedCommand): + name = "obsolete-attachments" + help_text = "Mark all attachments on a bug as obsolete" + argument_names = "BUGID" + steps = [ + steps.ObsoletePatches, + ] + + def _prepare_state(self, options, args, tool): + return { "bug_id" : args[0] } + + +class AttachToBug(AbstractSequencedCommand): + name = "attach-to-bug" + help_text = "Attach the the file to the bug" + argument_names = "BUGID FILEPATH" + steps = [ + steps.AttachToBug, + ] + + def _prepare_state(self, options, args, tool): + state = {} + state["bug_id"] = args[0] + state["filepath"] = args[1] + return state + + +class AbstractPatchUploadingCommand(AbstractSequencedCommand): + def _bug_id(self, options, args, tool, state): + # Perfer a bug id passed as an argument over a bug url in the diff (i.e. ChangeLogs). + bug_id = args and args[0] + if not bug_id: + changed_files = self._tool.scm().changed_files(options.git_commit) + state["changed_files"] = changed_files + bug_id = tool.checkout().bug_id_for_this_commit(options.git_commit, changed_files) + return bug_id + + def _prepare_state(self, options, args, tool): + state = {} + state["bug_id"] = self._bug_id(options, args, tool, state) + if not state["bug_id"]: + error("No bug id passed and no bug url found in ChangeLogs.") + return state + + +class Post(AbstractPatchUploadingCommand): + name = "post" + help_text = "Attach the current working directory diff to a bug as a patch file" + argument_names = "[BUGID]" + steps = [ + steps.ValidateChangeLogs, + steps.CheckStyle, + steps.ConfirmDiff, + steps.ObsoletePatches, + steps.SuggestReviewers, + steps.EnsureBugIsOpenAndAssigned, + steps.PostDiff, + ] + + +class LandSafely(AbstractPatchUploadingCommand): + name = "land-safely" + help_text = "Land the current diff via the commit-queue" + argument_names = "[BUGID]" + long_help = """land-safely updates the ChangeLog with the reviewer listed + in bugs.webkit.org for BUGID (or the bug ID detected from the ChangeLog). + The command then uploads the current diff to the bug and marks it for + commit by the commit-queue.""" + show_in_main_help = True + steps = [ + steps.UpdateChangeLogsWithReviewer, + steps.ValidateChangeLogs, + steps.ObsoletePatches, + steps.EnsureBugIsOpenAndAssigned, + steps.PostDiffForCommit, + ] + + +class Prepare(AbstractSequencedCommand): + name = "prepare" + help_text = "Creates a bug (or prompts for an existing bug) and prepares the ChangeLogs" + argument_names = "[BUGID]" + steps = [ + steps.PromptForBugOrTitle, + steps.CreateBug, + steps.PrepareChangeLog, + ] + + def _prepare_state(self, options, args, tool): + bug_id = args and args[0] + return { "bug_id" : bug_id } + + +class Upload(AbstractPatchUploadingCommand): + name = "upload" + help_text = "Automates the process of uploading a patch for review" + argument_names = "[BUGID]" + show_in_main_help = True + steps = [ + steps.ValidateChangeLogs, + steps.CheckStyle, + steps.PromptForBugOrTitle, + steps.CreateBug, + steps.PrepareChangeLog, + steps.EditChangeLog, + steps.ConfirmDiff, + steps.ObsoletePatches, + steps.SuggestReviewers, + steps.EnsureBugIsOpenAndAssigned, + steps.PostDiff, + ] + long_help = """upload uploads the current diff to bugs.webkit.org. + If no bug id is provided, upload will create a bug. + If the current diff does not have a ChangeLog, upload + will prepare a ChangeLog. Once a patch is read, upload + will open the ChangeLogs for editing using the command in the + EDITOR environment variable and will display the diff using the + command in the PAGER environment variable.""" + + def _prepare_state(self, options, args, tool): + state = {} + state["bug_id"] = self._bug_id(options, args, tool, state) + return state + + +class EditChangeLogs(AbstractSequencedCommand): + name = "edit-changelogs" + help_text = "Opens modified ChangeLogs in $EDITOR" + show_in_main_help = True + steps = [ + steps.EditChangeLog, + ] + + +class PostCommits(AbstractDeclarativeCommand): + name = "post-commits" + help_text = "Attach a range of local commits to bugs as patch files" + argument_names = "COMMITISH" + + def __init__(self): + options = [ + make_option("-b", "--bug-id", action="store", type="string", dest="bug_id", help="Specify bug id if no URL is provided in the commit log."), + make_option("--add-log-as-comment", action="store_true", dest="add_log_as_comment", default=False, help="Add commit log message as a comment when uploading the patch."), + make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: description from commit message)"), + steps.Options.obsolete_patches, + steps.Options.review, + steps.Options.request_commit, + ] + AbstractDeclarativeCommand.__init__(self, options=options, requires_local_commits=True) + + def _comment_text_for_commit(self, options, commit_message, tool, commit_id): + comment_text = None + if (options.add_log_as_comment): + comment_text = commit_message.body(lstrip=True) + comment_text += "---\n" + comment_text += tool.scm().files_changed_summary_for_commit(commit_id) + return comment_text + + def execute(self, options, args, tool): + commit_ids = tool.scm().commit_ids_from_commitish_arguments(args) + if len(commit_ids) > 10: # We could lower this limit, 10 is too many for one bug as-is. + error("webkit-patch does not support attaching %s at once. Are you sure you passed the right commit range?" % (pluralize("patch", len(commit_ids)))) + + have_obsoleted_patches = set() + for commit_id in commit_ids: + commit_message = tool.scm().commit_message_for_local_commit(commit_id) + + # Prefer --bug-id=, then a bug url in the commit message, then a bug url in the entire commit diff (i.e. ChangeLogs). + bug_id = options.bug_id or parse_bug_id_from_changelog(commit_message.message()) or parse_bug_id_from_changelog(tool.scm().create_patch(git_commit=commit_id)) + if not bug_id: + log("Skipping %s: No bug id found in commit or specified with --bug-id." % commit_id) + continue + + if options.obsolete_patches and bug_id not in have_obsoleted_patches: + state = { "bug_id": bug_id } + steps.ObsoletePatches(tool, options).run(state) + have_obsoleted_patches.add(bug_id) + + diff = tool.scm().create_patch(git_commit=commit_id) + description = options.description or commit_message.description(lstrip=True, strip_url=True) + comment_text = self._comment_text_for_commit(options, commit_message, tool, commit_id) + tool.bugs.add_patch_to_bug(bug_id, diff, description, comment_text, mark_for_review=options.review, mark_for_commit_queue=options.request_commit) + + +# FIXME: This command needs to be brought into the modern age with steps and CommitInfo. +class MarkBugFixed(AbstractDeclarativeCommand): + name = "mark-bug-fixed" + help_text = "Mark the specified bug as fixed" + argument_names = "[SVN_REVISION]" + def __init__(self): + options = [ + make_option("--bug-id", action="store", type="string", dest="bug_id", help="Specify bug id if no URL is provided in the commit log."), + make_option("--comment", action="store", type="string", dest="comment", help="Text to include in bug comment."), + make_option("--open", action="store_true", default=False, dest="open_bug", help="Open bug in default web browser (Mac only)."), + make_option("--update-only", action="store_true", default=False, dest="update_only", help="Add comment to the bug, but do not close it."), + ] + AbstractDeclarativeCommand.__init__(self, options=options) + + # FIXME: We should be using checkout().changelog_entries_for_revision(...) instead here. + def _fetch_commit_log(self, tool, svn_revision): + if not svn_revision: + return tool.scm().last_svn_commit_log() + return tool.scm().svn_commit_log(svn_revision) + + def _determine_bug_id_and_svn_revision(self, tool, bug_id, svn_revision): + commit_log = self._fetch_commit_log(tool, svn_revision) + + if not bug_id: + bug_id = parse_bug_id_from_changelog(commit_log) + + if not svn_revision: + match = re.search("^r(?P<svn_revision>\d+) \|", commit_log, re.MULTILINE) + if match: + svn_revision = match.group('svn_revision') + + if not bug_id or not svn_revision: + not_found = [] + if not bug_id: + not_found.append("bug id") + if not svn_revision: + not_found.append("svn revision") + error("Could not find %s on command-line or in %s." + % (" or ".join(not_found), "r%s" % svn_revision if svn_revision else "last commit")) + + return (bug_id, svn_revision) + + def execute(self, options, args, tool): + bug_id = options.bug_id + + svn_revision = args and args[0] + if svn_revision: + if re.match("^r[0-9]+$", svn_revision, re.IGNORECASE): + svn_revision = svn_revision[1:] + if not re.match("^[0-9]+$", svn_revision): + error("Invalid svn revision: '%s'" % svn_revision) + + needs_prompt = False + if not bug_id or not svn_revision: + needs_prompt = True + (bug_id, svn_revision) = self._determine_bug_id_and_svn_revision(tool, bug_id, svn_revision) + + log("Bug: <%s> %s" % (tool.bugs.bug_url_for_bug_id(bug_id), tool.bugs.fetch_bug_dictionary(bug_id)["title"])) + log("Revision: %s" % svn_revision) + + if options.open_bug: + tool.user.open_url(tool.bugs.bug_url_for_bug_id(bug_id)) + + if needs_prompt: + if not tool.user.confirm("Is this correct?"): + exit(1) + + bug_comment = bug_comment_from_svn_revision(svn_revision) + if options.comment: + bug_comment = "%s\n\n%s" % (options.comment, bug_comment) + + if options.update_only: + log("Adding comment to Bug %s." % bug_id) + tool.bugs.post_comment_to_bug(bug_id, bug_comment) + else: + log("Adding comment to Bug %s and marking as Resolved/Fixed." % bug_id) + tool.bugs.close_bug_as_fixed(bug_id, bug_comment) + + +# FIXME: Requires unit test. Blocking issue: too complex for now. +class CreateBug(AbstractDeclarativeCommand): + name = "create-bug" + help_text = "Create a bug from local changes or local commits" + argument_names = "[COMMITISH]" + + def __init__(self): + options = [ + steps.Options.cc, + steps.Options.component, + make_option("--no-prompt", action="store_false", dest="prompt", default=True, help="Do not prompt for bug title and comment; use commit log instead."), + make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review."), + make_option("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after review."), + ] + AbstractDeclarativeCommand.__init__(self, options=options) + + def create_bug_from_commit(self, options, args, tool): + commit_ids = tool.scm().commit_ids_from_commitish_arguments(args) + if len(commit_ids) > 3: + error("Are you sure you want to create one bug with %s patches?" % len(commit_ids)) + + commit_id = commit_ids[0] + + bug_title = "" + comment_text = "" + if options.prompt: + (bug_title, comment_text) = self.prompt_for_bug_title_and_comment() + else: + commit_message = tool.scm().commit_message_for_local_commit(commit_id) + bug_title = commit_message.description(lstrip=True, strip_url=True) + comment_text = commit_message.body(lstrip=True) + comment_text += "---\n" + comment_text += tool.scm().files_changed_summary_for_commit(commit_id) + + diff = tool.scm().create_patch(git_commit=commit_id) + bug_id = tool.bugs.create_bug(bug_title, comment_text, options.component, diff, "Patch", cc=options.cc, mark_for_review=options.review, mark_for_commit_queue=options.request_commit) + + if bug_id and len(commit_ids) > 1: + options.bug_id = bug_id + options.obsolete_patches = False + # FIXME: We should pass through --no-comment switch as well. + PostCommits.execute(self, options, commit_ids[1:], tool) + + def create_bug_from_patch(self, options, args, tool): + bug_title = "" + comment_text = "" + if options.prompt: + (bug_title, comment_text) = self.prompt_for_bug_title_and_comment() + else: + commit_message = tool.checkout().commit_message_for_this_commit(options.git_commit) + bug_title = commit_message.description(lstrip=True, strip_url=True) + comment_text = commit_message.body(lstrip=True) + + diff = tool.scm().create_patch(options.git_commit) + bug_id = tool.bugs.create_bug(bug_title, comment_text, options.component, diff, "Patch", cc=options.cc, mark_for_review=options.review, mark_for_commit_queue=options.request_commit) + + def prompt_for_bug_title_and_comment(self): + bug_title = User.prompt("Bug title: ") + print "Bug comment (hit ^D on blank line to end):" + lines = sys.stdin.readlines() + try: + sys.stdin.seek(0, os.SEEK_END) + except IOError: + # Cygwin raises an Illegal Seek (errno 29) exception when the above + # seek() call is made. Ignoring it seems to cause no harm. + # FIXME: Figure out a way to get avoid the exception in the first + # place. + pass + comment_text = "".join(lines) + return (bug_title, comment_text) + + def execute(self, options, args, tool): + if len(args): + if (not tool.scm().supports_local_commits()): + error("Extra arguments not supported; patch is taken from working directory.") + self.create_bug_from_commit(options, args, tool) + else: + self.create_bug_from_patch(options, args, tool) diff --git a/Tools/Scripts/webkitpy/tool/commands/upload_unittest.py b/Tools/Scripts/webkitpy/tool/commands/upload_unittest.py new file mode 100644 index 000000000..0ab0ede8f --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/upload_unittest.py @@ -0,0 +1,149 @@ +# Copyright (C) 2009 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. + +from webkitpy.thirdparty.mock import Mock +from webkitpy.tool.commands.commandtest import CommandsTest +from webkitpy.tool.commands.upload import * +from webkitpy.tool.mocktool import MockOptions, MockTool + +class UploadCommandsTest(CommandsTest): + def test_commit_message_for_current_diff(self): + tool = MockTool() + expected_stdout = "This is a fake commit message that is at least 50 characters.\n" + self.assert_execute_outputs(CommitMessageForCurrentDiff(), [], expected_stdout=expected_stdout, tool=tool) + + def test_clean_pending_commit(self): + self.assert_execute_outputs(CleanPendingCommit(), []) + + def test_assign_to_committer(self): + tool = MockTool() + expected_stderr = """Warning, attachment 10001 on bug 50000 has invalid committer (non-committer@example.com) +MOCK reassign_bug: bug_id=50000, assignee=eric@webkit.org +-- Begin comment -- +Attachment 10001 was posted by a committer and has review+, assigning to Eric Seidel for commit. +-- End comment -- +Bug 50003 is already assigned to foo@foo.com (None). +Bug 50002 has no non-obsolete patches, ignoring. +""" + self.assert_execute_outputs(AssignToCommitter(), [], expected_stderr=expected_stderr, tool=tool) + + def test_obsolete_attachments(self): + expected_stderr = "Obsoleting 2 old patches on bug 50000\n" + self.assert_execute_outputs(ObsoleteAttachments(), [50000], expected_stderr=expected_stderr) + + def test_post(self): + options = MockOptions() + options.cc = None + options.check_style = True + options.check_style_filter = None + options.comment = None + options.description = "MOCK description" + options.request_commit = False + options.review = True + options.suggest_reviewers = False + expected_stderr = """MOCK: user.open_url: file://... +Was that diff correct? +Obsoleting 2 old patches on bug 50000 +MOCK reassign_bug: bug_id=50000, assignee=None +MOCK add_patch_to_bug: bug_id=50000, description=MOCK description, mark_for_review=True, mark_for_commit_queue=False, mark_for_landing=False +MOCK: user.open_url: http://example.com/50000 +""" + self.assert_execute_outputs(Post(), [50000], options=options, expected_stderr=expected_stderr) + + def test_attach_to_bug(self): + options = MockOptions() + options.comment = "extra comment" + options.description = "file description" + expected_stderr = """MOCK add_attachment_to_bug: bug_id=50000, description=file description filename=None +-- Begin comment -- +extra comment +-- End comment -- +""" + self.assert_execute_outputs(AttachToBug(), [50000, "path/to/file.txt", "file description"], options=options, expected_stderr=expected_stderr) + + def test_attach_to_bug_no_description_or_comment(self): + options = MockOptions() + options.comment = None + options.description = None + expected_stderr = """MOCK add_attachment_to_bug: bug_id=50000, description=file.txt filename=None +""" + self.assert_execute_outputs(AttachToBug(), [50000, "path/to/file.txt"], options=options, expected_stderr=expected_stderr) + + def test_land_safely(self): + expected_stderr = "Obsoleting 2 old patches on bug 50000\nMOCK reassign_bug: bug_id=50000, assignee=None\nMOCK add_patch_to_bug: bug_id=50000, description=Patch for landing, mark_for_review=False, mark_for_commit_queue=False, mark_for_landing=True\n" + self.assert_execute_outputs(LandSafely(), [50000], expected_stderr=expected_stderr) + + def test_prepare_diff_with_arg(self): + self.assert_execute_outputs(Prepare(), [50000]) + + def test_prepare(self): + expected_stderr = "MOCK create_bug\nbug_title: Mock user response\nbug_description: Mock user response\ncomponent: MOCK component\ncc: MOCK cc\n" + self.assert_execute_outputs(Prepare(), [], expected_stderr=expected_stderr) + + def test_upload(self): + options = MockOptions() + options.cc = None + options.check_style = True + options.check_style_filter = None + options.comment = None + options.description = "MOCK description" + options.request_commit = False + options.review = True + options.suggest_reviewers = False + expected_stderr = """MOCK: user.open_url: file://... +Was that diff correct? +Obsoleting 2 old patches on bug 50000 +MOCK reassign_bug: bug_id=50000, assignee=None +MOCK add_patch_to_bug: bug_id=50000, description=MOCK description, mark_for_review=True, mark_for_commit_queue=False, mark_for_landing=False +MOCK: user.open_url: http://example.com/50000 +""" + self.assert_execute_outputs(Upload(), [50000], options=options, expected_stderr=expected_stderr) + + def test_mark_bug_fixed(self): + tool = MockTool() + tool._scm.last_svn_commit_log = lambda: "r9876 |" + options = Mock() + options.bug_id = 50000 + options.comment = "MOCK comment" + expected_stderr = """Bug: <http://example.com/50000> Bug with two r+'d and cq+'d patches, one of which has an invalid commit-queue setter. +Revision: 9876 +MOCK: user.open_url: http://example.com/50000 +Is this correct? +Adding comment to Bug 50000. +MOCK bug comment: bug_id=50000, cc=None +--- Begin comment --- +MOCK comment + +Committed r9876: <http://trac.webkit.org/changeset/9876> +--- End comment --- + +""" + self.assert_execute_outputs(MarkBugFixed(), [], expected_stderr=expected_stderr, tool=tool, options=options) + + def test_edit_changelog(self): + self.assert_execute_outputs(EditChangeLogs(), []) |