123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491 |
- # 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.
- import codecs
- import logging
- import os
- import sys
- import time
- import traceback
- from datetime import datetime
- from optparse import make_option
- from StringIO import StringIO
- from webkitpy.common.config.committervalidator import CommitterValidator
- from webkitpy.common.config.ports import DeprecatedPort
- from webkitpy.common.net.bugzilla import Attachment
- from webkitpy.common.net.statusserver import StatusServer
- 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.flakytestreporter import FlakyTestReporter
- from webkitpy.tool.bot.layouttestresultsreader import LayoutTestResultsReader
- from webkitpy.tool.bot.patchanalysistask import UnableToApplyPatch
- from webkitpy.tool.bot.queueengine import QueueEngine, QueueEngineDelegate
- from webkitpy.tool.bot.stylequeuetask import StyleQueueTask, StyleQueueTaskDelegate
- from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler
- from webkitpy.tool.multicommandtool import Command, TryAgain
- _log = logging.getLogger(__name__)
- 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."),
- ]
- self.help_text = "Run the %s" % self.name
- Command.__init__(self, 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.error("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)
- try:
- args_for_printing = list(webkit_patch_args)
- args_for_printing[0] = 'webkit-patch' # Printing our path for each log is redundant.
- _log.info("Running: %s" % self._tool.executive.command_for_printing(args_for_printing))
- command_output = self._tool.executive.run_command(webkit_patch_args, cwd=self._tool.scm().checkout_root)
- except ScriptError, e:
- # Make sure the whole output gets printed if the command failed.
- _log.error(e.message_with_output(output_limit=None))
- raise
- return command_output
- 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.info("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"):
- _log.error("User declined.")
- sys.exit(1)
- _log.info("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 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, self._options.seconds_to_sleep).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 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.error(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):
- # FIXME: Bugzilla accessibility should be checked here; if it's unaccessible,
- # it should return None.
- patch = None
- while not patch:
- 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.
- fake_patch = Attachment({'id': patch_id}, None)
- self._release_work_item(fake_patch)
- 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)
- def work_item_log_path(self, patch):
- return os.path.join(self._log_directory(), "%s.log" % patch.bug_id())
- # Used to share code between the EWS and commit-queue.
- class PatchProcessingQueue(AbstractPatchQueue):
- # Subclasses must override.
- port_name = None
- def __init__(self, options=None):
- self._port = None # We can't instantiate port here because tool isn't avaialble.
- AbstractPatchQueue.__init__(self, options)
- # FIXME: This is a hack to map between the old port names and the new port names.
- def _new_port_name_from_old(self, port_name, platform):
- # ApplePort.determine_full_port_name asserts if the name doesn't include version.
- if port_name == 'mac':
- return 'mac-' + platform.os_version
- if port_name == 'win':
- return 'win-future'
- return port_name
- def begin_work_queue(self):
- AbstractPatchQueue.begin_work_queue(self)
- if not self.port_name:
- return
- # FIXME: This is only used for self._deprecated_port.flag()
- self._deprecated_port = DeprecatedPort.port(self.port_name)
- # FIXME: This violates abstraction
- self._tool._deprecated_port = self._deprecated_port
- self._port = self._tool.port_factory.get(self._new_port_name_from_old(self.port_name, self._tool.platform))
- def _upload_results_archive_for_patch(self, patch, results_archive_zip):
- if not self._port:
- self._port = self._tool.port_factory.get(self._new_port_name_from_old(self.port_name, self._tool.platform))
- bot_id = self._tool.status_server.bot_id or "bot"
- description = "Archive of layout-test-results from %s for %s" % (bot_id, self._port.name())
- # 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, self._port.name()).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)
- class CommitQueue(PatchProcessingQueue, StepSequenceErrorHandler, CommitQueueTaskDelegate):
- name = "commit-queue"
- port_name = "mac-mountainlion"
- # AbstractPatchQueue methods
- def begin_work_queue(self):
- PatchProcessingQueue.begin_work_queue(self)
- self.committer_validator = CommitterValidator(self._tool)
- self._expected_failures = ExpectedFailures()
- self._layout_test_results_reader = LayoutTestResultsReader(self._tool, self._port.results_directory(), self._log_directory())
- def next_work_item(self):
- return self._next_patch()
- 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 + [self._deprecated_port.flag()])
- 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 test_results(self):
- return self._layout_test_results_reader.results()
- def archive_last_test_results(self, patch):
- return self._layout_test_results_reader.archive(patch)
- def build_style(self):
- return "release"
- 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)
- def did_pass_testing_ews(self, patch):
- # Only Mac and Mac WK2 run tests
- # FIXME: We shouldn't have to hard-code it here.
- patch_status = self._tool.status_server.patch_status
- return patch_status("mac-ews", patch.id()) == self._pass_status or patch_status("mac-wk2-ews", patch.id()) == self._pass_status
- # StepSequenceErrorHandler methods
- @classmethod
- 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.error(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(PatchProcessingQueue, StepSequenceErrorHandler):
- """This is the base-class for the EWS queues and the style-queue."""
- def __init__(self, options=None):
- PatchProcessingQueue.__init__(self, options)
- def review_patch(self, patch):
- raise NotImplementedError("subclasses must implement")
- # AbstractPatchQueue methods
- def begin_work_queue(self):
- PatchProcessingQueue.begin_work_queue(self)
- def next_work_item(self):
- return self._next_patch()
- 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.error(message)
- # StepSequenceErrorHandler methods
- @classmethod
- def handle_script_error(cls, tool, state, script_error):
- _log.error(script_error.output)
- class StyleQueue(AbstractReviewQueue, StyleQueueTaskDelegate):
- name = "style-queue"
- def __init__(self):
- AbstractReviewQueue.__init__(self)
- def review_patch(self, patch):
- task = StyleQueueTask(self, patch)
- 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:
- 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." % (patch.id(), self.name, e.output)
- self._tool.bugs.post_comment_to_bug(patch.bug_id(), message, cc=self.watchers)
- self._did_fail(patch)
- return False
- return True
- # StyleQueueTaskDelegate 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 None
- def refetch_patch(self, patch):
- return self._tool.bugs.fetch_attachment(patch.id())
|