Home | History | Annotate | Download | only in commands
      1 # Copyright (c) 2010 Google Inc. All rights reserved.
      2 #
      3 # Redistribution and use in source and binary forms, with or without
      4 # modification, are permitted provided that the following conditions are
      5 # met:
      6 #
      7 #     * Redistributions of source code must retain the above copyright
      8 # notice, this list of conditions and the following disclaimer.
      9 #     * Redistributions in binary form must reproduce the above
     10 # copyright notice, this list of conditions and the following disclaimer
     11 # in the documentation and/or other materials provided with the
     12 # distribution.
     13 #     * Neither the name of Google Inc. nor the names of its
     14 # contributors may be used to endorse or promote products derived from
     15 # this software without specific prior written permission.
     16 #
     17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     28 
     29 import json
     30 import logging
     31 import optparse
     32 import re
     33 import sys
     34 import time
     35 import urllib2
     36 
     37 from webkitpy.common.checkout.baselineoptimizer import BaselineOptimizer
     38 from webkitpy.common.memoized import memoized
     39 from webkitpy.common.system.executive import ScriptError
     40 from webkitpy.layout_tests.controllers.test_result_writer import TestResultWriter
     41 from webkitpy.layout_tests.models import test_failures
     42 from webkitpy.layout_tests.models.test_expectations import TestExpectations, BASELINE_SUFFIX_LIST
     43 from webkitpy.layout_tests.port import builders
     44 from webkitpy.layout_tests.port import factory
     45 from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
     46 
     47 
     48 _log = logging.getLogger(__name__)
     49 
     50 
     51 # FIXME: Should TestResultWriter know how to compute this string?
     52 def _baseline_name(fs, test_name, suffix):
     53     return fs.splitext(test_name)[0] + TestResultWriter.FILENAME_SUFFIX_EXPECTED + "." + suffix
     54 
     55 
     56 class AbstractRebaseliningCommand(AbstractDeclarativeCommand):
     57     # not overriding execute() - pylint: disable=W0223
     58 
     59     no_optimize_option = optparse.make_option('--no-optimize', dest='optimize', action='store_false', default=True,
     60         help=('Do not optimize/de-dup the expectations after rebaselining (default is to de-dup automatically). '
     61               'You can use "webkit-patch optimize-baselines" to optimize separately.'))
     62 
     63     platform_options = factory.platform_options(use_globs=True)
     64 
     65     results_directory_option = optparse.make_option("--results-directory", help="Local results directory to use")
     66 
     67     suffixes_option = optparse.make_option("--suffixes", default=','.join(BASELINE_SUFFIX_LIST), action="store",
     68         help="Comma-separated-list of file types to rebaseline")
     69 
     70     def __init__(self, options=None):
     71         super(AbstractRebaseliningCommand, self).__init__(options=options)
     72         self._baseline_suffix_list = BASELINE_SUFFIX_LIST
     73 
     74 
     75 class BaseInternalRebaselineCommand(AbstractRebaseliningCommand):
     76     def __init__(self):
     77         super(BaseInternalRebaselineCommand, self).__init__(options=[
     78             self.results_directory_option,
     79             self.suffixes_option,
     80             optparse.make_option("--builder", help="Builder to pull new baselines from"),
     81             optparse.make_option("--test", help="Test to rebaseline"),
     82             ])
     83         self._scm_changes = {'add': [], 'remove-lines': []}
     84 
     85     def _add_to_scm(self, path):
     86         self._scm_changes['add'].append(path)
     87 
     88     def _baseline_directory(self, builder_name):
     89         port = self._tool.port_factory.get_from_builder_name(builder_name)
     90         override_dir = builders.rebaseline_override_dir(builder_name)
     91         if override_dir:
     92             return self._tool.filesystem.join(port.layout_tests_dir(), 'platform', override_dir)
     93         return port.baseline_version_dir()
     94 
     95     def _test_root(self, test_name):
     96         return self._tool.filesystem.splitext(test_name)[0]
     97 
     98     def _file_name_for_actual_result(self, test_name, suffix):
     99         return "%s-actual.%s" % (self._test_root(test_name), suffix)
    100 
    101     def _file_name_for_expected_result(self, test_name, suffix):
    102         return "%s-expected.%s" % (self._test_root(test_name), suffix)
    103 
    104 
    105 class CopyExistingBaselinesInternal(BaseInternalRebaselineCommand):
    106     name = "copy-existing-baselines-internal"
    107     help_text = "Copy existing baselines down one level in the baseline order to ensure new baselines don't break existing passing platforms."
    108 
    109     @memoized
    110     def _immediate_predecessors_in_fallback(self, path_to_rebaseline):
    111         port_names = self._tool.port_factory.all_port_names()
    112         immediate_predecessors_in_fallback = []
    113         for port_name in port_names:
    114             port = self._tool.port_factory.get(port_name)
    115             if not port.buildbot_archives_baselines():
    116                 continue
    117             baseline_search_path = port.baseline_search_path()
    118             try:
    119                 index = baseline_search_path.index(path_to_rebaseline)
    120                 if index:
    121                     immediate_predecessors_in_fallback.append(self._tool.filesystem.basename(baseline_search_path[index - 1]))
    122             except ValueError:
    123                 # index throw's a ValueError if the item isn't in the list.
    124                 pass
    125         return immediate_predecessors_in_fallback
    126 
    127     def _port_for_primary_baseline(self, baseline):
    128         for port in [self._tool.port_factory.get(port_name) for port_name in self._tool.port_factory.all_port_names()]:
    129             if self._tool.filesystem.basename(port.baseline_version_dir()) == baseline:
    130                 return port
    131         raise Exception("Failed to find port for primary baseline %s." % baseline)
    132 
    133     def _copy_existing_baseline(self, builder_name, test_name, suffix):
    134         baseline_directory = self._baseline_directory(builder_name)
    135         ports = [self._port_for_primary_baseline(baseline) for baseline in self._immediate_predecessors_in_fallback(baseline_directory)]
    136 
    137         old_baselines = []
    138         new_baselines = []
    139 
    140         # Need to gather all the baseline paths before modifying the filesystem since
    141         # the modifications can affect the results of port.expected_filename.
    142         for port in ports:
    143             old_baseline = port.expected_filename(test_name, "." + suffix)
    144             if not self._tool.filesystem.exists(old_baseline):
    145                 _log.debug("No existing baseline for %s." % test_name)
    146                 continue
    147 
    148             new_baseline = self._tool.filesystem.join(port.baseline_path(), self._file_name_for_expected_result(test_name, suffix))
    149             if self._tool.filesystem.exists(new_baseline):
    150                 _log.debug("Existing baseline at %s, not copying over it." % new_baseline)
    151                 continue
    152 
    153             old_baselines.append(old_baseline)
    154             new_baselines.append(new_baseline)
    155 
    156         for i in range(len(old_baselines)):
    157             old_baseline = old_baselines[i]
    158             new_baseline = new_baselines[i]
    159 
    160             _log.debug("Copying baseline from %s to %s." % (old_baseline, new_baseline))
    161             self._tool.filesystem.maybe_make_directory(self._tool.filesystem.dirname(new_baseline))
    162             self._tool.filesystem.copyfile(old_baseline, new_baseline)
    163             if not self._tool.scm().exists(new_baseline):
    164                 self._add_to_scm(new_baseline)
    165 
    166     def execute(self, options, args, tool):
    167         for suffix in options.suffixes.split(','):
    168             self._copy_existing_baseline(options.builder, options.test, suffix)
    169         print json.dumps(self._scm_changes)
    170 
    171 
    172 class RebaselineTest(BaseInternalRebaselineCommand):
    173     name = "rebaseline-test-internal"
    174     help_text = "Rebaseline a single test from a buildbot. Only intended for use by other webkit-patch commands."
    175 
    176     def _results_url(self, builder_name):
    177         return self._tool.buildbot_for_builder_name(builder_name).builder_with_name(builder_name).latest_layout_test_results_url()
    178 
    179     def _save_baseline(self, data, target_baseline, baseline_directory, test_name, suffix):
    180         if not data:
    181             _log.debug("No baseline data to save.")
    182             return
    183 
    184         filesystem = self._tool.filesystem
    185         filesystem.maybe_make_directory(filesystem.dirname(target_baseline))
    186         filesystem.write_binary_file(target_baseline, data)
    187         if not self._tool.scm().exists(target_baseline):
    188             self._add_to_scm(target_baseline)
    189 
    190     def _rebaseline_test(self, builder_name, test_name, suffix, results_url):
    191         baseline_directory = self._baseline_directory(builder_name)
    192 
    193         source_baseline = "%s/%s" % (results_url, self._file_name_for_actual_result(test_name, suffix))
    194         target_baseline = self._tool.filesystem.join(baseline_directory, self._file_name_for_expected_result(test_name, suffix))
    195 
    196         _log.debug("Retrieving %s." % source_baseline)
    197         self._save_baseline(self._tool.web.get_binary(source_baseline, convert_404_to_None=True), target_baseline, baseline_directory, test_name, suffix)
    198 
    199     def _rebaseline_test_and_update_expectations(self, options):
    200         port = self._tool.port_factory.get_from_builder_name(options.builder)
    201         if (port.reference_files(options.test)):
    202             _log.warning("Cannot rebaseline reftest: %s", options.test)
    203             return
    204 
    205         if options.results_directory:
    206             results_url = 'file://' + options.results_directory
    207         else:
    208             results_url = self._results_url(options.builder)
    209         self._baseline_suffix_list = options.suffixes.split(',')
    210 
    211         for suffix in self._baseline_suffix_list:
    212             self._rebaseline_test(options.builder, options.test, suffix, results_url)
    213         self._scm_changes['remove-lines'].append({'builder': options.builder, 'test': options.test})
    214 
    215     def execute(self, options, args, tool):
    216         self._rebaseline_test_and_update_expectations(options)
    217         print json.dumps(self._scm_changes)
    218 
    219 
    220 class OptimizeBaselines(AbstractRebaseliningCommand):
    221     name = "optimize-baselines"
    222     help_text = "Reshuffles the baselines for the given tests to use as litte space on disk as possible."
    223     argument_names = "TEST_NAMES"
    224 
    225     def __init__(self):
    226         super(OptimizeBaselines, self).__init__(options=[self.suffixes_option] + self.platform_options)
    227 
    228     def _optimize_baseline(self, optimizer, test_name):
    229         for suffix in self._baseline_suffix_list:
    230             baseline_name = _baseline_name(self._tool.filesystem, test_name, suffix)
    231             if not optimizer.optimize(baseline_name):
    232                 print "Heuristics failed to optimize %s" % baseline_name
    233 
    234     def execute(self, options, args, tool):
    235         self._baseline_suffix_list = options.suffixes.split(',')
    236         port_names = tool.port_factory.all_port_names(options.platform)
    237         if not port_names:
    238             print "No port names match '%s'" % options.platform
    239             return
    240 
    241         optimizer = BaselineOptimizer(tool, port_names)
    242         port = tool.port_factory.get(port_names[0])
    243         for test_name in port.tests(args):
    244             _log.info("Optimizing %s" % test_name)
    245             self._optimize_baseline(optimizer, test_name)
    246 
    247 
    248 class AnalyzeBaselines(AbstractRebaseliningCommand):
    249     name = "analyze-baselines"
    250     help_text = "Analyzes the baselines for the given tests and prints results that are identical."
    251     argument_names = "TEST_NAMES"
    252 
    253     def __init__(self):
    254         super(AnalyzeBaselines, self).__init__(options=[
    255             self.suffixes_option,
    256             optparse.make_option('--missing', action='store_true', default=False, help='show missing baselines as well'),
    257             ] + self.platform_options)
    258         self._optimizer_class = BaselineOptimizer  # overridable for testing
    259         self._baseline_optimizer = None
    260         self._port = None
    261 
    262     def _write(self, msg):
    263         print msg
    264 
    265     def _analyze_baseline(self, options, test_name):
    266         for suffix in self._baseline_suffix_list:
    267             baseline_name = _baseline_name(self._tool.filesystem, test_name, suffix)
    268             results_by_directory = self._baseline_optimizer.read_results_by_directory(baseline_name)
    269             if results_by_directory:
    270                 self._write("%s:" % baseline_name)
    271                 self._baseline_optimizer.write_by_directory(results_by_directory, self._write, "  ")
    272             elif options.missing:
    273                 self._write("%s: (no baselines found)" % baseline_name)
    274 
    275     def execute(self, options, args, tool):
    276         self._baseline_suffix_list = options.suffixes.split(',')
    277         port_names = tool.port_factory.all_port_names(options.platform)
    278         if not port_names:
    279             print "No port names match '%s'" % options.platform
    280             return
    281 
    282         self._baseline_optimizer = self._optimizer_class(tool, port_names)
    283         self._port = tool.port_factory.get(port_names[0])
    284         for test_name in self._port.tests(args):
    285             self._analyze_baseline(options, test_name)
    286 
    287 
    288 class AbstractParallelRebaselineCommand(AbstractRebaseliningCommand):
    289     # not overriding execute() - pylint: disable=W0223
    290 
    291     def __init__(self, options=None):
    292         super(AbstractParallelRebaselineCommand, self).__init__(options=options)
    293         self._builder_data = {}
    294 
    295     def builder_data(self):
    296         if not self._builder_data:
    297             for builder_name in self._release_builders():
    298                 builder = self._tool.buildbot_for_builder_name(builder_name).builder_with_name(builder_name)
    299                 self._builder_data[builder_name] = builder.latest_layout_test_results()
    300         return self._builder_data
    301 
    302     # The release builders cycle much faster than the debug ones and cover all the platforms.
    303     def _release_builders(self):
    304         release_builders = []
    305         for builder_name in builders.all_builder_names():
    306             port = self._tool.port_factory.get_from_builder_name(builder_name)
    307             if port.test_configuration().build_type == 'release':
    308                 release_builders.append(builder_name)
    309         return release_builders
    310 
    311     def _run_webkit_patch(self, args, verbose):
    312         try:
    313             verbose_args = ['--verbose'] if verbose else []
    314             stderr = self._tool.executive.run_command([self._tool.path()] + verbose_args + args, cwd=self._tool.scm().checkout_root, return_stderr=True)
    315             for line in stderr.splitlines():
    316                 _log.warning(line)
    317         except ScriptError, e:
    318             _log.error(e)
    319 
    320     def _builders_to_fetch_from(self, builders_to_check):
    321         # This routine returns the subset of builders that will cover all of the baseline search paths
    322         # used in the input list. In particular, if the input list contains both Release and Debug
    323         # versions of a configuration, we *only* return the Release version (since we don't save
    324         # debug versions of baselines).
    325         release_builders = set()
    326         debug_builders = set()
    327         builders_to_fallback_paths = {}
    328         for builder in builders_to_check:
    329             port = self._tool.port_factory.get_from_builder_name(builder)
    330             if port.test_configuration().build_type == 'release':
    331                 release_builders.add(builder)
    332             else:
    333                 debug_builders.add(builder)
    334         for builder in list(release_builders) + list(debug_builders):
    335             port = self._tool.port_factory.get_from_builder_name(builder)
    336             fallback_path = port.baseline_search_path()
    337             if fallback_path not in builders_to_fallback_paths.values():
    338                 builders_to_fallback_paths[builder] = fallback_path
    339         return builders_to_fallback_paths.keys()
    340 
    341     def _rebaseline_commands(self, test_prefix_list, options):
    342         path_to_webkit_patch = self._tool.path()
    343         cwd = self._tool.scm().checkout_root
    344         copy_baseline_commands = []
    345         rebaseline_commands = []
    346         port = self._tool.port_factory.get()
    347 
    348         for test_prefix in test_prefix_list:
    349             for test in port.tests([test_prefix]):
    350                 for builder in self._builders_to_fetch_from(test_prefix_list[test_prefix]):
    351                     actual_failures_suffixes = self._suffixes_for_actual_failures(test, builder, test_prefix_list[test_prefix][builder])
    352                     if not actual_failures_suffixes:
    353                         continue
    354 
    355                     suffixes = ','.join(actual_failures_suffixes)
    356                     cmd_line = ['--suffixes', suffixes, '--builder', builder, '--test', test]
    357                     if options.results_directory:
    358                         cmd_line.extend(['--results-directory', options.results_directory])
    359                     if options.verbose:
    360                         cmd_line.append('--verbose')
    361                     copy_baseline_commands.append(tuple([[path_to_webkit_patch, 'copy-existing-baselines-internal'] + cmd_line, cwd]))
    362                     rebaseline_commands.append(tuple([[path_to_webkit_patch, 'rebaseline-test-internal'] + cmd_line, cwd]))
    363         return copy_baseline_commands, rebaseline_commands
    364 
    365     def _files_to_add(self, command_results):
    366         files_to_add = set()
    367         lines_to_remove = {}
    368         for output in [result[1].split('\n') for result in command_results]:
    369             file_added = False
    370             for line in output:
    371                 try:
    372                     if line:
    373                         parsed_line = json.loads(line)
    374                         if 'add' in parsed_line:
    375                             files_to_add.update(parsed_line['add'])
    376                         if 'remove-lines' in parsed_line:
    377                             for line_to_remove in parsed_line['remove-lines']:
    378                                 test = line_to_remove['test']
    379                                 builder = line_to_remove['builder']
    380                                 if test not in lines_to_remove:
    381                                     lines_to_remove[test] = []
    382                                 lines_to_remove[test].append(builder)
    383                         file_added = True
    384                 except ValueError:
    385                     _log.debug('"%s" is not a JSON object, ignoring' % line)
    386 
    387             if not file_added:
    388                 _log.debug('Could not add file based off output "%s"' % output)
    389 
    390         return list(files_to_add), lines_to_remove
    391 
    392     def _optimize_baselines(self, test_prefix_list, verbose=False):
    393         # We don't run this in parallel because modifying the SCM in parallel is unreliable.
    394         for test in test_prefix_list:
    395             all_suffixes = set()
    396             for builder in self._builders_to_fetch_from(test_prefix_list[test]):
    397                 all_suffixes.update(self._suffixes_for_actual_failures(test, builder, test_prefix_list[test][builder]))
    398             # FIXME: We should propagate the platform options as well.
    399             self._run_webkit_patch(['optimize-baselines', '--suffixes', ','.join(all_suffixes), test], verbose)
    400 
    401     def _update_expectations_files(self, lines_to_remove):
    402         for test in lines_to_remove:
    403             for builder in lines_to_remove[test]:
    404                 port = self._tool.port_factory.get_from_builder_name(builder)
    405                 path = port.path_to_generic_test_expectations_file()
    406                 expectations = TestExpectations(port, include_overrides=False)
    407                 for test_configuration in port.all_test_configurations():
    408                     if test_configuration.version == port.test_configuration().version:
    409                         expectationsString = expectations.remove_configuration_from_test(test, test_configuration)
    410                 self._tool.filesystem.write_text_file(path, expectationsString)
    411 
    412     def _run_in_parallel_and_update_scm(self, commands):
    413         command_results = self._tool.executive.run_in_parallel(commands)
    414         log_output = '\n'.join(result[2] for result in command_results).replace('\n\n', '\n')
    415         for line in log_output.split('\n'):
    416             if line:
    417                 print >> sys.stderr, line  # FIXME: Figure out how to log properly.
    418 
    419         files_to_add, lines_to_remove = self._files_to_add(command_results)
    420         if files_to_add:
    421             self._tool.scm().add_list(list(files_to_add))
    422         if lines_to_remove:
    423             self._update_expectations_files(lines_to_remove)
    424 
    425     def _rebaseline(self, options, test_prefix_list):
    426         for test, builders_to_check in sorted(test_prefix_list.items()):
    427             _log.info("Rebaselining %s" % test)
    428             for builder, suffixes in sorted(builders_to_check.items()):
    429                 _log.debug("  %s: %s" % (builder, ",".join(suffixes)))
    430 
    431         copy_baseline_commands, rebaseline_commands = self._rebaseline_commands(test_prefix_list, options)
    432         self._run_in_parallel_and_update_scm(copy_baseline_commands)
    433         self._run_in_parallel_and_update_scm(rebaseline_commands)
    434 
    435         if options.optimize:
    436             self._optimize_baselines(test_prefix_list, options.verbose)
    437 
    438     def _suffixes_for_actual_failures(self, test, builder_name, existing_suffixes):
    439         actual_results = self.builder_data()[builder_name].actual_results(test)
    440         if not actual_results:
    441             return set()
    442         return set(existing_suffixes) & TestExpectations.suffixes_for_actual_expectations_string(actual_results)
    443 
    444 
    445 class RebaselineJson(AbstractParallelRebaselineCommand):
    446     name = "rebaseline-json"
    447     help_text = "Rebaseline based off JSON passed to stdin. Intended to only be called from other scripts."
    448 
    449     def __init__(self,):
    450         super(RebaselineJson, self).__init__(options=[
    451             self.no_optimize_option,
    452             self.results_directory_option,
    453             ])
    454 
    455     def execute(self, options, args, tool):
    456         self._rebaseline(options, json.loads(sys.stdin.read()))
    457 
    458 
    459 class RebaselineExpectations(AbstractParallelRebaselineCommand):
    460     name = "rebaseline-expectations"
    461     help_text = "Rebaselines the tests indicated in TestExpectations."
    462 
    463     def __init__(self):
    464         super(RebaselineExpectations, self).__init__(options=[
    465             self.no_optimize_option,
    466             ] + self.platform_options)
    467         self._test_prefix_list = None
    468 
    469     def _tests_to_rebaseline(self, port):
    470         tests_to_rebaseline = {}
    471         for path, value in port.expectations_dict().items():
    472             expectations = TestExpectations(port, include_overrides=False, expectations_dict={path: value})
    473             for test in expectations.get_rebaselining_failures():
    474                 suffixes = TestExpectations.suffixes_for_expectations(expectations.get_expectations(test))
    475                 tests_to_rebaseline[test] = suffixes or BASELINE_SUFFIX_LIST
    476         return tests_to_rebaseline
    477 
    478     def _add_tests_to_rebaseline_for_port(self, port_name):
    479         builder_name = builders.builder_name_for_port_name(port_name)
    480         if not builder_name:
    481             return
    482         tests = self._tests_to_rebaseline(self._tool.port_factory.get(port_name)).items()
    483 
    484         if tests:
    485             _log.info("Retrieving results for %s from %s." % (port_name, builder_name))
    486 
    487         for test_name, suffixes in tests:
    488             _log.info("    %s (%s)" % (test_name, ','.join(suffixes)))
    489             if test_name not in self._test_prefix_list:
    490                 self._test_prefix_list[test_name] = {}
    491             self._test_prefix_list[test_name][builder_name] = suffixes
    492 
    493     def execute(self, options, args, tool):
    494         options.results_directory = None
    495         self._test_prefix_list = {}
    496         port_names = tool.port_factory.all_port_names(options.platform)
    497         for port_name in port_names:
    498             self._add_tests_to_rebaseline_for_port(port_name)
    499         if not self._test_prefix_list:
    500             _log.warning("Did not find any tests marked Rebaseline.")
    501             return
    502 
    503         self._rebaseline(options, self._test_prefix_list)
    504 
    505 
    506 class Rebaseline(AbstractParallelRebaselineCommand):
    507     name = "rebaseline"
    508     help_text = "Rebaseline tests with results from the build bots. Shows the list of failing tests on the builders if no test names are provided."
    509     argument_names = "[TEST_NAMES]"
    510 
    511     def __init__(self):
    512         super(Rebaseline, self).__init__(options=[
    513             self.no_optimize_option,
    514             # FIXME: should we support the platform options in addition to (or instead of) --builders?
    515             self.suffixes_option,
    516             self.results_directory_option,
    517             optparse.make_option("--builders", default=None, action="append", help="Comma-separated-list of builders to pull new baselines from (can also be provided multiple times)"),
    518             ])
    519 
    520     def _builders_to_pull_from(self):
    521         chosen_names = self._tool.user.prompt_with_list("Which builder to pull results from:", self._release_builders(), can_choose_multiple=True)
    522         return [self._builder_with_name(name) for name in chosen_names]
    523 
    524     def _builder_with_name(self, name):
    525         return self._tool.buildbot_for_builder_name(name).builder_with_name(name)
    526 
    527     def execute(self, options, args, tool):
    528         if not args:
    529             _log.error("Must list tests to rebaseline.")
    530             return
    531 
    532         if options.builders:
    533             builders_to_check = []
    534             for builder_names in options.builders:
    535                 builders_to_check += [self._builder_with_name(name) for name in builder_names.split(",")]
    536         else:
    537             builders_to_check = self._builders_to_pull_from()
    538 
    539         test_prefix_list = {}
    540         suffixes_to_update = options.suffixes.split(",")
    541 
    542         for builder in builders_to_check:
    543             for test in args:
    544                 if test not in test_prefix_list:
    545                     test_prefix_list[test] = {}
    546                 test_prefix_list[test][builder.name()] = suffixes_to_update
    547 
    548         if options.verbose:
    549             _log.debug("rebaseline-json: " + str(test_prefix_list))
    550 
    551         self._rebaseline(options, test_prefix_list)
    552 
    553 
    554 class AutoRebaseline(AbstractParallelRebaselineCommand):
    555     name = "auto-rebaseline"
    556     help_text = "Rebaselines any NeedsRebaseline lines in TestExpectations that have cycled through all the bots."
    557     AUTO_REBASELINE_BRANCH_NAME = "auto-rebaseline-temporary-branch"
    558 
    559     # Rietveld uploader stinks. Limit the number of rebaselines in a given patch to keep upload from failing.
    560     # FIXME: http://crbug.com/263676 Obviously we should fix the uploader here.
    561     MAX_LINES_TO_REBASELINE = 200
    562 
    563     def __init__(self):
    564         super(AutoRebaseline, self).__init__(options=[
    565             # FIXME: Remove this option.
    566             self.no_optimize_option,
    567             # FIXME: Remove this option.
    568             self.results_directory_option,
    569             ])
    570 
    571     def latest_revision_processed_on_all_bots(self):
    572         revisions = []
    573         for result in self.builder_data().values():
    574             if result.run_was_interrupted():
    575                 _log.error("Can't rebaseline. The latest run on %s did not complete." % builder_name)
    576                 return 0
    577             revisions.append(result.blink_revision())
    578         return int(min(revisions))
    579 
    580     def tests_to_rebaseline(self, tool, min_revision, print_revisions):
    581         port = tool.port_factory.get()
    582         expectations_file_path = port.path_to_generic_test_expectations_file()
    583 
    584         tests = set()
    585         revision = None
    586         author = None
    587         bugs = set()
    588 
    589         for line in tool.scm().blame(expectations_file_path).split("\n"):
    590             if "NeedsRebaseline" not in line:
    591                 continue
    592             parsed_line = re.match("^(\S*)[^(]*\((\S*).*?([^ ]*)\ \[[^[]*$", line)
    593 
    594             commit_hash = parsed_line.group(1)
    595             svn_revision = tool.scm().svn_revision_from_git_commit(commit_hash)
    596 
    597             test = parsed_line.group(3)
    598             if print_revisions:
    599                 _log.info("%s is waiting for r%s" % (test, svn_revision))
    600 
    601             if not svn_revision or svn_revision > min_revision:
    602                 continue
    603 
    604             if revision and svn_revision != revision:
    605                 continue
    606 
    607             if not revision:
    608                 revision = svn_revision
    609                 author = parsed_line.group(2)
    610 
    611             bugs.update(re.findall("crbug\.com\/(\d+)", line))
    612             tests.add(test)
    613 
    614             if len(tests) >= self.MAX_LINES_TO_REBASELINE:
    615                 _log.info("Too many tests to rebaseline in one patch. Doing the first %d." % self.MAX_LINES_TO_REBASELINE)
    616                 break
    617 
    618         return tests, revision, author, bugs
    619 
    620     def link_to_patch(self, revision):
    621         return "http://src.chromium.org/viewvc/blink?view=revision&revision=" + str(revision)
    622 
    623     def commit_message(self, author, revision, bugs):
    624         bug_string = ""
    625         if bugs:
    626             bug_string = "BUG=%s\n" % ",".join(bugs)
    627 
    628         return """Auto-rebaseline for r%s
    629 
    630 %s
    631 
    632 %sTBR=%s
    633 """ % (revision, self.link_to_patch(revision), bug_string, author)
    634 
    635     def get_test_prefix_list(self, tests):
    636         test_prefix_list = {}
    637         lines_to_remove = {}
    638 
    639         for builder_name in self._release_builders():
    640             port_name = builders.port_name_for_builder_name(builder_name)
    641             port = self._tool.port_factory.get(port_name)
    642             expectations = TestExpectations(port, include_overrides=True)
    643             for test in expectations.get_needs_rebaseline_failures():
    644                 if test not in tests:
    645                     continue
    646 
    647                 if test not in test_prefix_list:
    648                     lines_to_remove[test] = []
    649                     test_prefix_list[test] = {}
    650                 lines_to_remove[test].append(builder_name)
    651                 test_prefix_list[test][builder_name] = BASELINE_SUFFIX_LIST
    652 
    653         return test_prefix_list, lines_to_remove
    654 
    655     def _run_git_cl_command(self, options, command):
    656         subprocess_command = ['git', 'cl'] + command
    657         if options.verbose:
    658             subprocess_command.append('--verbose')
    659         # Use call instead of run_command so that stdout doesn't get swallowed.
    660         self._tool.executive.call(subprocess_command)
    661 
    662     # FIXME: Move this somewhere more general.
    663     def tree_status(self):
    664         blink_tree_status_url = "http://blink-status.appspot.com/status"
    665         status = urllib2.urlopen(blink_tree_status_url).read().lower()
    666         if status.find('closed') != -1 or status == 0:
    667             return 'closed'
    668         elif status.find('open') != -1 or status == 1:
    669             return 'open'
    670         return 'unknown'
    671 
    672     def execute(self, options, args, tool):
    673         if tool.scm().executable_name == "svn":
    674             _log.error("Auto rebaseline only works with a git checkout.")
    675             return
    676 
    677         if tool.scm().has_working_directory_changes():
    678             _log.error("Cannot proceed with working directory changes. Clean working directory first.")
    679             return
    680 
    681         min_revision = self.latest_revision_processed_on_all_bots()
    682         if not min_revision:
    683             return
    684 
    685         if options.verbose:
    686             _log.info("Bot min revision is %s." % min_revision)
    687 
    688         tests, revision, author, bugs = self.tests_to_rebaseline(tool, min_revision, print_revisions=options.verbose)
    689         test_prefix_list, lines_to_remove = self.get_test_prefix_list(tests)
    690 
    691         if not tests:
    692             _log.debug('No tests to rebaseline.')
    693             return
    694         _log.info('Rebaselining %s for r%s by %s.' % (list(tests), revision, author))
    695 
    696         if self.tree_status() == 'closed':
    697             _log.info('Cannot proceed. Tree is closed.')
    698             return
    699 
    700         try:
    701             old_branch_name = tool.scm().current_branch()
    702             tool.scm().delete_branch(self.AUTO_REBASELINE_BRANCH_NAME)
    703             tool.scm().create_clean_branch(self.AUTO_REBASELINE_BRANCH_NAME)
    704 
    705             # If the tests are passing everywhere, then this list will be empty. We don't need
    706             # to rebaseline, but we'll still need to update TestExpectations.
    707             if test_prefix_list:
    708                 self._rebaseline(options, test_prefix_list)
    709             # If a test is not failing on the bot, we don't try to rebaseline it, but we still
    710             # want to remove the NeedsRebaseline line.
    711             self._update_expectations_files(lines_to_remove)
    712 
    713             tool.scm().commit_locally_with_message(self.commit_message(author, revision, bugs))
    714 
    715             # FIXME: It would be nice if we could dcommit the patch without uploading, but still
    716             # go through all the precommit hooks. For rebaselines with lots of files, uploading
    717             # takes a long time and sometimes fails, but we don't want to commit if, e.g. the
    718             # tree is closed.
    719             self._run_git_cl_command(options, ['upload', '-f'])
    720 
    721             # Uploading can take a very long time. Do another pull to make sure TestExpectations is up to date,
    722             # so the dcommit can go through.
    723             tool.executive.run_command(['git', 'pull'])
    724 
    725             self._run_git_cl_command(options, ['dcommit', '-f'])
    726         finally:
    727             self._run_git_cl_command(options, ['set_close'])
    728             tool.scm().ensure_cleanly_tracking_remote_master()
    729             tool.scm().checkout_branch(old_branch_name)
    730             tool.scm().delete_branch(self.AUTO_REBASELINE_BRANCH_NAME)
    731 
    732 
    733 class RebaselineOMatic(AbstractDeclarativeCommand):
    734     name = "rebaseline-o-matic"
    735     help_text = "Calls webkit-patch auto-rebaseline in a loop."
    736 
    737     SLEEP_TIME_IN_SECONDS = 30
    738 
    739     def execute(self, options, args, tool):
    740         while True:
    741             tool.executive.run_command(['git', 'pull'])
    742             rebaseline_command = [tool.filesystem.join(tool.scm().checkout_root, 'Tools', 'Scripts', 'webkit-patch'), 'auto-rebaseline']
    743             if options.verbose:
    744                 rebaseline_command.append('--verbose')
    745             # Use call instead of run_command so that stdout doesn't get swallowed.
    746             tool.executive.call(rebaseline_command)
    747             time.sleep(self.SLEEP_TIME_IN_SECONDS)
    748