1 # Copyright (c) 2009 Google Inc. All rights reserved. 2 # Copyright (c) 2009 Apple Inc. All rights reserved. 3 # 4 # Redistribution and use in source and binary forms, with or without 5 # modification, are permitted provided that the following conditions are 6 # met: 7 # 8 # * Redistributions of source code must retain the above copyright 9 # notice, this list of conditions and the following disclaimer. 10 # * Redistributions in binary form must reproduce the above 11 # copyright notice, this list of conditions and the following disclaimer 12 # in the documentation and/or other materials provided with the 13 # distribution. 14 # * Neither the name of Google Inc. nor the names of its 15 # contributors may be used to endorse or promote products derived from 16 # this software without specific prior written permission. 17 # 18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 30 from __future__ import with_statement 31 32 import codecs 33 import time 34 import traceback 35 import os 36 37 from datetime import datetime 38 from optparse import make_option 39 from StringIO import StringIO 40 41 from webkitpy.common.config.committervalidator import CommitterValidator 42 from webkitpy.common.net.bugzilla import Attachment 43 from webkitpy.common.net.layouttestresults import LayoutTestResults 44 from webkitpy.common.net.statusserver import StatusServer 45 from webkitpy.common.system.deprecated_logging import error, log 46 from webkitpy.common.system.executive import ScriptError 47 from webkitpy.tool.bot.botinfo import BotInfo 48 from webkitpy.tool.bot.commitqueuetask import CommitQueueTask, CommitQueueTaskDelegate 49 from webkitpy.tool.bot.feeders import CommitQueueFeeder, EWSFeeder 50 from webkitpy.tool.bot.queueengine import QueueEngine, QueueEngineDelegate 51 from webkitpy.tool.bot.flakytestreporter import FlakyTestReporter 52 from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler 53 from webkitpy.tool.steps.runtests import RunTests 54 from webkitpy.tool.multicommandtool import Command, TryAgain 55 56 57 class AbstractQueue(Command, QueueEngineDelegate): 58 watchers = [ 59 ] 60 61 _pass_status = "Pass" 62 _fail_status = "Fail" 63 _retry_status = "Retry" 64 _error_status = "Error" 65 66 def __init__(self, options=None): # Default values should never be collections (like []) as default values are shared between invocations 67 options_list = (options or []) + [ 68 make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Do not ask the user for confirmation before running the queue. Dangerous!"), 69 make_option("--exit-after-iteration", action="store", type="int", dest="iterations", default=None, help="Stop running the queue after iterating this number of times."), 70 ] 71 Command.__init__(self, "Run the %s" % self.name, options=options_list) 72 self._iteration_count = 0 73 74 def _cc_watchers(self, bug_id): 75 try: 76 self._tool.bugs.add_cc_to_bug(bug_id, self.watchers) 77 except Exception, e: 78 traceback.print_exc() 79 log("Failed to CC watchers.") 80 81 def run_webkit_patch(self, args): 82 webkit_patch_args = [self._tool.path()] 83 # FIXME: This is a hack, we should have a more general way to pass global options. 84 # FIXME: We must always pass global options and their value in one argument 85 # because our global option code looks for the first argument which does 86 # not begin with "-" and assumes that is the command name. 87 webkit_patch_args += ["--status-host=%s" % self._tool.status_server.host] 88 if self._tool.status_server.bot_id: 89 webkit_patch_args += ["--bot-id=%s" % self._tool.status_server.bot_id] 90 if self._options.port: 91 webkit_patch_args += ["--port=%s" % self._options.port] 92 webkit_patch_args.extend(args) 93 # FIXME: There is probably no reason to use run_and_throw_if_fail anymore. 94 # run_and_throw_if_fail was invented to support tee'd output 95 # (where we write both to a log file and to the console at once), 96 # but the queues don't need live-progress, a dump-of-output at the 97 # end should be sufficient. 98 return self._tool.executive.run_and_throw_if_fail(webkit_patch_args) 99 100 def _log_directory(self): 101 return os.path.join("..", "%s-logs" % self.name) 102 103 # QueueEngineDelegate methods 104 105 def queue_log_path(self): 106 return os.path.join(self._log_directory(), "%s.log" % self.name) 107 108 def work_item_log_path(self, work_item): 109 raise NotImplementedError, "subclasses must implement" 110 111 def begin_work_queue(self): 112 log("CAUTION: %s will discard all local changes in \"%s\"" % (self.name, self._tool.scm().checkout_root)) 113 if self._options.confirm: 114 response = self._tool.user.prompt("Are you sure? Type \"yes\" to continue: ") 115 if (response != "yes"): 116 error("User declined.") 117 log("Running WebKit %s." % self.name) 118 self._tool.status_server.update_status(self.name, "Starting Queue") 119 120 def stop_work_queue(self, reason): 121 self._tool.status_server.update_status(self.name, "Stopping Queue, reason: %s" % reason) 122 123 def should_continue_work_queue(self): 124 self._iteration_count += 1 125 return not self._options.iterations or self._iteration_count <= self._options.iterations 126 127 def next_work_item(self): 128 raise NotImplementedError, "subclasses must implement" 129 130 def should_proceed_with_work_item(self, work_item): 131 raise NotImplementedError, "subclasses must implement" 132 133 def process_work_item(self, work_item): 134 raise NotImplementedError, "subclasses must implement" 135 136 def handle_unexpected_error(self, work_item, message): 137 raise NotImplementedError, "subclasses must implement" 138 139 # Command methods 140 141 def execute(self, options, args, tool, engine=QueueEngine): 142 self._options = options # FIXME: This code is wrong. Command.options is a list, this assumes an Options element! 143 self._tool = tool # FIXME: This code is wrong too! Command.bind_to_tool handles this! 144 return engine(self.name, self, self._tool.wakeup_event).run() 145 146 @classmethod 147 def _log_from_script_error_for_upload(cls, script_error, output_limit=None): 148 # We have seen request timeouts with app engine due to large 149 # log uploads. Trying only the last 512k. 150 if not output_limit: 151 output_limit = 512 * 1024 # 512k 152 output = script_error.message_with_output(output_limit=output_limit) 153 # We pre-encode the string to a byte array before passing it 154 # to status_server, because ClientForm (part of mechanize) 155 # wants a file-like object with pre-encoded data. 156 return StringIO(output.encode("utf-8")) 157 158 @classmethod 159 def _update_status_for_script_error(cls, tool, state, script_error, is_error=False): 160 message = str(script_error) 161 if is_error: 162 message = "Error: %s" % message 163 failure_log = cls._log_from_script_error_for_upload(script_error) 164 return tool.status_server.update_status(cls.name, message, state["patch"], failure_log) 165 166 167 class FeederQueue(AbstractQueue): 168 name = "feeder-queue" 169 170 _sleep_duration = 30 # seconds 171 172 # AbstractPatchQueue methods 173 174 def begin_work_queue(self): 175 AbstractQueue.begin_work_queue(self) 176 self.feeders = [ 177 CommitQueueFeeder(self._tool), 178 EWSFeeder(self._tool), 179 ] 180 181 def next_work_item(self): 182 # This really show inherit from some more basic class that doesn't 183 # understand work items, but the base class in the heirarchy currently 184 # understands work items. 185 return "synthetic-work-item" 186 187 def should_proceed_with_work_item(self, work_item): 188 return True 189 190 def process_work_item(self, work_item): 191 for feeder in self.feeders: 192 feeder.feed() 193 time.sleep(self._sleep_duration) 194 return True 195 196 def work_item_log_path(self, work_item): 197 return None 198 199 def handle_unexpected_error(self, work_item, message): 200 log(message) 201 202 203 class AbstractPatchQueue(AbstractQueue): 204 def _update_status(self, message, patch=None, results_file=None): 205 return self._tool.status_server.update_status(self.name, message, patch, results_file) 206 207 def _next_patch(self): 208 patch_id = self._tool.status_server.next_work_item(self.name) 209 if not patch_id: 210 return None 211 patch = self._tool.bugs.fetch_attachment(patch_id) 212 if not patch: 213 # FIXME: Using a fake patch because release_work_item has the wrong API. 214 # We also don't really need to release the lock (although that's fine), 215 # mostly we just need to remove this bogus patch from our queue. 216 # If for some reason bugzilla is just down, then it will be re-fed later. 217 patch = Attachment({'id': patch_id}, None) 218 self._release_work_item(patch) 219 return None 220 return patch 221 222 def _release_work_item(self, patch): 223 self._tool.status_server.release_work_item(self.name, patch) 224 225 def _did_pass(self, patch): 226 self._update_status(self._pass_status, patch) 227 self._release_work_item(patch) 228 229 def _did_fail(self, patch): 230 self._update_status(self._fail_status, patch) 231 self._release_work_item(patch) 232 233 def _did_retry(self, patch): 234 self._update_status(self._retry_status, patch) 235 self._release_work_item(patch) 236 237 def _did_error(self, patch, reason): 238 message = "%s: %s" % (self._error_status, reason) 239 self._update_status(message, patch) 240 self._release_work_item(patch) 241 242 def work_item_log_path(self, patch): 243 return os.path.join(self._log_directory(), "%s.log" % patch.bug_id()) 244 245 246 class CommitQueue(AbstractPatchQueue, StepSequenceErrorHandler, CommitQueueTaskDelegate): 247 name = "commit-queue" 248 249 # AbstractPatchQueue methods 250 251 def begin_work_queue(self): 252 AbstractPatchQueue.begin_work_queue(self) 253 self.committer_validator = CommitterValidator(self._tool.bugs) 254 255 def next_work_item(self): 256 return self._next_patch() 257 258 def should_proceed_with_work_item(self, patch): 259 patch_text = "rollout patch" if patch.is_rollout() else "patch" 260 self._update_status("Processing %s" % patch_text, patch) 261 return True 262 263 # FIXME: This is not really specific to the commit-queue and could be shared. 264 def _upload_results_archive_for_patch(self, patch, results_archive_zip): 265 bot_id = self._tool.status_server.bot_id or "bot" 266 description = "Archive of layout-test-results from %s" % bot_id 267 # results_archive is a ZipFile object, grab the File object (.fp) to pass to Mechanize for uploading. 268 results_archive_file = results_archive_zip.fp 269 # Rewind the file object to start (since Mechanize won't do that automatically) 270 # See https://bugs.webkit.org/show_bug.cgi?id=54593 271 results_archive_file.seek(0) 272 comment_text = "The attached test failures were seen while running run-webkit-tests on the %s.\n" % (self.name) 273 # FIXME: We could easily list the test failures from the archive here. 274 comment_text += BotInfo(self._tool).summary_text() 275 self._tool.bugs.add_attachment_to_bug(patch.bug_id(), results_archive_file, description, filename="layout-test-results.zip", comment_text=comment_text) 276 277 def process_work_item(self, patch): 278 self._cc_watchers(patch.bug_id()) 279 task = CommitQueueTask(self, patch) 280 try: 281 if task.run(): 282 self._did_pass(patch) 283 return True 284 self._did_retry(patch) 285 except ScriptError, e: 286 validator = CommitterValidator(self._tool.bugs) 287 validator.reject_patch_from_commit_queue(patch.id(), self._error_message_for_bug(task.failure_status_id, e)) 288 results_archive = task.results_archive_from_patch_test_run(patch) 289 if results_archive: 290 self._upload_results_archive_for_patch(patch, results_archive) 291 self._did_fail(patch) 292 293 def _error_message_for_bug(self, status_id, script_error): 294 if not script_error.output: 295 return script_error.message_with_output() 296 results_link = self._tool.status_server.results_url_for_status(status_id) 297 return "%s\nFull output: %s" % (script_error.message_with_output(), results_link) 298 299 def handle_unexpected_error(self, patch, message): 300 self.committer_validator.reject_patch_from_commit_queue(patch.id(), message) 301 302 # CommitQueueTaskDelegate methods 303 304 def run_command(self, command): 305 self.run_webkit_patch(command) 306 307 def command_passed(self, message, patch): 308 self._update_status(message, patch=patch) 309 310 def command_failed(self, message, script_error, patch): 311 failure_log = self._log_from_script_error_for_upload(script_error) 312 return self._update_status(message, patch=patch, results_file=failure_log) 313 314 # FIXME: This exists for mocking, but should instead be mocked via 315 # tool.filesystem.read_text_file. They have different error handling at the moment. 316 def _read_file_contents(self, path): 317 try: 318 return self._tool.filesystem.read_text_file(path) 319 except IOError, e: # File does not exist or can't be read. 320 return None 321 322 # FIXME: This logic should move to the port object. 323 def _create_layout_test_results(self): 324 results_path = self._tool.port().layout_tests_results_path() 325 results_html = self._read_file_contents(results_path) 326 if not results_html: 327 return None 328 return LayoutTestResults.results_from_string(results_html) 329 330 def layout_test_results(self): 331 results = self._create_layout_test_results() 332 # FIXME: We should not have to set failure_limit_count, but we 333 # do until run-webkit-tests can be updated save off the value 334 # of --exit-after-N-failures in results.html/results.json. 335 # https://bugs.webkit.org/show_bug.cgi?id=58481 336 if results: 337 results.set_failure_limit_count(RunTests.NON_INTERACTIVE_FAILURE_LIMIT_COUNT) 338 return results 339 340 def _results_directory(self): 341 results_path = self._tool.port().layout_tests_results_path() 342 # FIXME: This is wrong in two ways: 343 # 1. It assumes that results.html is at the top level of the results tree. 344 # 2. This uses the "old" ports.py infrastructure instead of the new layout_tests/port 345 # which will not support Chromium. However the new arch doesn't work with old-run-webkit-tests 346 # so we have to use this for now. 347 return os.path.dirname(results_path) 348 349 def archive_last_layout_test_results(self, patch): 350 results_directory = self._results_directory() 351 results_name, _ = os.path.splitext(os.path.basename(results_directory)) 352 # Note: We name the zip with the bug_id instead of patch_id to match work_item_log_path(). 353 zip_path = self._tool.workspace.find_unused_filename(self._log_directory(), "%s-%s" % (patch.bug_id(), results_name), "zip") 354 if not zip_path: 355 return None 356 archive = self._tool.workspace.create_zip(zip_path, results_directory) 357 # Remove the results directory to prevent http logs, etc. from getting huge between runs. 358 # We could have create_zip remove the original, but this is more explicit. 359 self._tool.filesystem.rmtree(results_directory) 360 return archive 361 362 def refetch_patch(self, patch): 363 return self._tool.bugs.fetch_attachment(patch.id()) 364 365 def report_flaky_tests(self, patch, flaky_test_results, results_archive=None): 366 reporter = FlakyTestReporter(self._tool, self.name) 367 reporter.report_flaky_tests(patch, flaky_test_results, results_archive) 368 369 # StepSequenceErrorHandler methods 370 371 def handle_script_error(cls, tool, state, script_error): 372 # Hitting this error handler should be pretty rare. It does occur, 373 # however, when a patch no longer applies to top-of-tree in the final 374 # land step. 375 log(script_error.message_with_output()) 376 377 @classmethod 378 def handle_checkout_needs_update(cls, tool, state, options, error): 379 message = "Tests passed, but commit failed (checkout out of date). Updating, then landing without building or re-running tests." 380 tool.status_server.update_status(cls.name, message, state["patch"]) 381 # The only time when we find out that out checkout needs update is 382 # when we were ready to actually pull the trigger and land the patch. 383 # Rather than spinning in the master process, we retry without 384 # building or testing, which is much faster. 385 options.build = False 386 options.test = False 387 options.update = True 388 raise TryAgain() 389 390 391 class AbstractReviewQueue(AbstractPatchQueue, StepSequenceErrorHandler): 392 """This is the base-class for the EWS queues and the style-queue.""" 393 def __init__(self, options=None): 394 AbstractPatchQueue.__init__(self, options) 395 396 def review_patch(self, patch): 397 raise NotImplementedError("subclasses must implement") 398 399 # AbstractPatchQueue methods 400 401 def begin_work_queue(self): 402 AbstractPatchQueue.begin_work_queue(self) 403 404 def next_work_item(self): 405 return self._next_patch() 406 407 def should_proceed_with_work_item(self, patch): 408 raise NotImplementedError("subclasses must implement") 409 410 def process_work_item(self, patch): 411 try: 412 if not self.review_patch(patch): 413 return False 414 self._did_pass(patch) 415 return True 416 except ScriptError, e: 417 if e.exit_code != QueueEngine.handled_error_code: 418 self._did_fail(patch) 419 else: 420 # The subprocess handled the error, but won't have released the patch, so we do. 421 # FIXME: We need to simplify the rules by which _release_work_item is called. 422 self._release_work_item(patch) 423 raise e 424 425 def handle_unexpected_error(self, patch, message): 426 log(message) 427 428 # StepSequenceErrorHandler methods 429 430 @classmethod 431 def handle_script_error(cls, tool, state, script_error): 432 log(script_error.message_with_output()) 433 434 435 class StyleQueue(AbstractReviewQueue): 436 name = "style-queue" 437 def __init__(self): 438 AbstractReviewQueue.__init__(self) 439 440 def should_proceed_with_work_item(self, patch): 441 self._update_status("Checking style", patch) 442 return True 443 444 def review_patch(self, patch): 445 self.run_webkit_patch(["check-style", "--force-clean", "--non-interactive", "--parent-command=style-queue", patch.id()]) 446 return True 447 448 @classmethod 449 def handle_script_error(cls, tool, state, script_error): 450 is_svn_apply = script_error.command_name() == "svn-apply" 451 status_id = cls._update_status_for_script_error(tool, state, script_error, is_error=is_svn_apply) 452 if is_svn_apply: 453 QueueEngine.exit_after_handled_error(script_error) 454 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)) 455 tool.bugs.post_comment_to_bug(state["patch"].bug_id(), message, cc=cls.watchers) 456 exit(1) 457