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 Queue
     30 import json
     31 import logging
     32 import optparse
     33 import re
     34 import sys
     35 import threading
     36 import time
     37 import traceback
     38 import urllib
     39 import urllib2
     40 
     41 from webkitpy.common.checkout.baselineoptimizer import BaselineOptimizer
     42 from webkitpy.common.memoized import memoized
     43 from webkitpy.common.system.executive import ScriptError
     44 from webkitpy.layout_tests.controllers.test_result_writer import TestResultWriter
     45 from webkitpy.layout_tests.models import test_failures
     46 from webkitpy.layout_tests.models.test_expectations import TestExpectations, BASELINE_SUFFIX_LIST, SKIP
     47 from webkitpy.layout_tests.port import builders
     48 from webkitpy.layout_tests.port import factory
     49 from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
     50 
     51 
     52 _log = logging.getLogger(__name__)
     53 
     54 
     55 # FIXME: Should TestResultWriter know how to compute this string?
     56 def _baseline_name(fs, test_name, suffix):
     57     return fs.splitext(test_name)[0] + TestResultWriter.FILENAME_SUFFIX_EXPECTED + "." + suffix
     58 
     59 
     60 class AbstractRebaseliningCommand(AbstractDeclarativeCommand):
     61     # not overriding execute() - pylint: disable=W0223
     62 
     63     no_optimize_option = optparse.make_option('--no-optimize', dest='optimize', action='store_false', default=True,
     64         help=('Do not optimize/de-dup the expectations after rebaselining (default is to de-dup automatically). '
     65               'You can use "webkit-patch optimize-baselines" to optimize separately.'))
     66 
     67     platform_options = factory.platform_options(use_globs=True)
     68 
     69     results_directory_option = optparse.make_option("--results-directory", help="Local results directory to use")
     70 
     71     suffixes_option = optparse.make_option("--suffixes", default=','.join(BASELINE_SUFFIX_LIST), action="store",
     72         help="Comma-separated-list of file types to rebaseline")
     73 
     74     def __init__(self, options=None):
     75         super(AbstractRebaseliningCommand, self).__init__(options=options)
     76         self._baseline_suffix_list = BASELINE_SUFFIX_LIST
     77         self._scm_changes = {'add': [], 'delete': [], 'remove-lines': []}
     78 
     79     def _add_to_scm_later(self, path):
     80         self._scm_changes['add'].append(path)
     81 
     82     def _delete_from_scm_later(self, path):
     83         self._scm_changes['delete'].append(path)
     84 
     85 
     86 class BaseInternalRebaselineCommand(AbstractRebaseliningCommand):
     87     def __init__(self):
     88         super(BaseInternalRebaselineCommand, self).__init__(options=[
     89             self.results_directory_option,
     90             self.suffixes_option,
     91             optparse.make_option("--builder", help="Builder to pull new baselines from"),
     92             optparse.make_option("--test", help="Test to rebaseline"),
     93             ])
     94 
     95     def _baseline_directory(self, builder_name):
     96         port = self._tool.port_factory.get_from_builder_name(builder_name)
     97         override_dir = builders.rebaseline_override_dir(builder_name)
     98         if override_dir:
     99             return self._tool.filesystem.join(port.layout_tests_dir(), 'platform', override_dir)
    100         return port.baseline_version_dir()
    101 
    102     def _test_root(self, test_name):
    103         return self._tool.filesystem.splitext(test_name)[0]
    104 
    105     def _file_name_for_actual_result(self, test_name, suffix):
    106         return "%s-actual.%s" % (self._test_root(test_name), suffix)
    107 
    108     def _file_name_for_expected_result(self, test_name, suffix):
    109         return "%s-expected.%s" % (self._test_root(test_name), suffix)
    110 
    111 
    112 class CopyExistingBaselinesInternal(BaseInternalRebaselineCommand):
    113     name = "copy-existing-baselines-internal"
    114     help_text = "Copy existing baselines down one level in the baseline order to ensure new baselines don't break existing passing platforms."
    115 
    116     @memoized
    117     def _immediate_predecessors_in_fallback(self, path_to_rebaseline):
    118         port_names = self._tool.port_factory.all_port_names()
    119         immediate_predecessors_in_fallback = []
    120         for port_name in port_names:
    121             port = self._tool.port_factory.get(port_name)
    122             if not port.buildbot_archives_baselines():
    123                 continue
    124             baseline_search_path = port.baseline_search_path()
    125             try:
    126                 index = baseline_search_path.index(path_to_rebaseline)
    127                 if index:
    128                     immediate_predecessors_in_fallback.append(self._tool.filesystem.basename(baseline_search_path[index - 1]))
    129             except ValueError:
    130                 # index throw's a ValueError if the item isn't in the list.
    131                 pass
    132         return immediate_predecessors_in_fallback
    133 
    134     def _port_for_primary_baseline(self, baseline):
    135         for port in [self._tool.port_factory.get(port_name) for port_name in self._tool.port_factory.all_port_names()]:
    136             if self._tool.filesystem.basename(port.baseline_version_dir()) == baseline:
    137                 return port
    138         raise Exception("Failed to find port for primary baseline %s." % baseline)
    139 
    140     def _copy_existing_baseline(self, builder_name, test_name, suffix):
    141         baseline_directory = self._baseline_directory(builder_name)
    142         ports = [self._port_for_primary_baseline(baseline) for baseline in self._immediate_predecessors_in_fallback(baseline_directory)]
    143 
    144         old_baselines = []
    145         new_baselines = []
    146 
    147         # Need to gather all the baseline paths before modifying the filesystem since
    148         # the modifications can affect the results of port.expected_filename.
    149         for port in ports:
    150             old_baseline = port.expected_filename(test_name, "." + suffix)
    151             if not self._tool.filesystem.exists(old_baseline):
    152                 _log.debug("No existing baseline for %s." % test_name)
    153                 continue
    154 
    155             new_baseline = self._tool.filesystem.join(port.baseline_path(), self._file_name_for_expected_result(test_name, suffix))
    156             if self._tool.filesystem.exists(new_baseline):
    157                 _log.debug("Existing baseline at %s, not copying over it." % new_baseline)
    158                 continue
    159 
    160             expectations = TestExpectations(port, [test_name])
    161             if SKIP in expectations.get_expectations(test_name):
    162                 _log.debug("%s is skipped on %s." % (test_name, port.name()))
    163                 continue
    164 
    165             old_baselines.append(old_baseline)
    166             new_baselines.append(new_baseline)
    167 
    168         for i in range(len(old_baselines)):
    169             old_baseline = old_baselines[i]
    170             new_baseline = new_baselines[i]
    171 
    172             _log.debug("Copying baseline from %s to %s." % (old_baseline, new_baseline))
    173             self._tool.filesystem.maybe_make_directory(self._tool.filesystem.dirname(new_baseline))
    174             self._tool.filesystem.copyfile(old_baseline, new_baseline)
    175             if not self._tool.scm().exists(new_baseline):
    176                 self._add_to_scm_later(new_baseline)
    177 
    178     def execute(self, options, args, tool):
    179         for suffix in options.suffixes.split(','):
    180             self._copy_existing_baseline(options.builder, options.test, suffix)
    181         print json.dumps(self._scm_changes)
    182 
    183 
    184 class RebaselineTest(BaseInternalRebaselineCommand):
    185     name = "rebaseline-test-internal"
    186     help_text = "Rebaseline a single test from a buildbot. Only intended for use by other webkit-patch commands."
    187 
    188     def _results_url(self, builder_name):
    189         return self._tool.buildbot_for_builder_name(builder_name).builder_with_name(builder_name).latest_layout_test_results_url()
    190 
    191     def _save_baseline(self, data, target_baseline, baseline_directory, test_name, suffix):
    192         if not data:
    193             _log.debug("No baseline data to save.")
    194             return
    195 
    196         filesystem = self._tool.filesystem
    197         filesystem.maybe_make_directory(filesystem.dirname(target_baseline))
    198         filesystem.write_binary_file(target_baseline, data)
    199         if not self._tool.scm().exists(target_baseline):
    200             self._add_to_scm_later(target_baseline)
    201 
    202     def _rebaseline_test(self, builder_name, test_name, suffix, results_url):
    203         baseline_directory = self._baseline_directory(builder_name)
    204 
    205         source_baseline = "%s/%s" % (results_url, self._file_name_for_actual_result(test_name, suffix))
    206         target_baseline = self._tool.filesystem.join(baseline_directory, self._file_name_for_expected_result(test_name, suffix))
    207 
    208         _log.debug("Retrieving %s." % source_baseline)
    209         self._save_baseline(self._tool.web.get_binary(source_baseline, convert_404_to_None=True), target_baseline, baseline_directory, test_name, suffix)
    210 
    211     def _rebaseline_test_and_update_expectations(self, options):
    212         port = self._tool.port_factory.get_from_builder_name(options.builder)
    213         if (port.reference_files(options.test)):
    214             _log.warning("Cannot rebaseline reftest: %s", options.test)
    215             return
    216 
    217         if options.results_directory:
    218             results_url = 'file://' + options.results_directory
    219         else:
    220             results_url = self._results_url(options.builder)
    221         self._baseline_suffix_list = options.suffixes.split(',')
    222 
    223         for suffix in self._baseline_suffix_list:
    224             self._rebaseline_test(options.builder, options.test, suffix, results_url)
    225         self._scm_changes['remove-lines'].append({'builder': options.builder, 'test': options.test})
    226 
    227     def execute(self, options, args, tool):
    228         self._rebaseline_test_and_update_expectations(options)
    229         print json.dumps(self._scm_changes)
    230 
    231 
    232 class OptimizeBaselines(AbstractRebaseliningCommand):
    233     name = "optimize-baselines"
    234     help_text = "Reshuffles the baselines for the given tests to use as litte space on disk as possible."
    235     show_in_main_help = True
    236     argument_names = "TEST_NAMES"
    237 
    238     def __init__(self):
    239         super(OptimizeBaselines, self).__init__(options=[
    240             self.suffixes_option,
    241             optparse.make_option('--no-modify-scm', action='store_true', default=False, help='Dump SCM commands as JSON instead of '),
    242             ] + self.platform_options)
    243 
    244     def _optimize_baseline(self, optimizer, test_name):
    245         files_to_delete = []
    246         files_to_add = []
    247         for suffix in self._baseline_suffix_list:
    248             baseline_name = _baseline_name(self._tool.filesystem, test_name, suffix)
    249             succeeded, more_files_to_delete, more_files_to_add = optimizer.optimize(baseline_name)
    250             if not succeeded:
    251                 print "Heuristics failed to optimize %s" % baseline_name
    252             files_to_delete.extend(more_files_to_delete)
    253             files_to_add.extend(more_files_to_add)
    254         return files_to_delete, files_to_add
    255 
    256     def execute(self, options, args, tool):
    257         self._baseline_suffix_list = options.suffixes.split(',')
    258         port_names = tool.port_factory.all_port_names(options.platform)
    259         if not port_names:
    260             print "No port names match '%s'" % options.platform
    261             return
    262         port = tool.port_factory.get(port_names[0])
    263         optimizer = BaselineOptimizer(tool, port, port_names, skip_scm_commands=options.no_modify_scm)
    264         tests = port.tests(args)
    265         for test_name in tests:
    266             files_to_delete, files_to_add = self._optimize_baseline(optimizer, test_name)
    267             for path in files_to_delete:
    268                 self._delete_from_scm_later(path)
    269             for path in files_to_add:
    270                 self._add_to_scm_later(path)
    271 
    272         print json.dumps(self._scm_changes)
    273 
    274 
    275 class AnalyzeBaselines(AbstractRebaseliningCommand):
    276     name = "analyze-baselines"
    277     help_text = "Analyzes the baselines for the given tests and prints results that are identical."
    278     show_in_main_help = True
    279     argument_names = "TEST_NAMES"
    280 
    281     def __init__(self):
    282         super(AnalyzeBaselines, self).__init__(options=[
    283             self.suffixes_option,
    284             optparse.make_option('--missing', action='store_true', default=False, help='show missing baselines as well'),
    285             ] + self.platform_options)
    286         self._optimizer_class = BaselineOptimizer  # overridable for testing
    287         self._baseline_optimizer = None
    288         self._port = None
    289 
    290     def _write(self, msg):
    291         print msg
    292 
    293     def _analyze_baseline(self, options, test_name):
    294         for suffix in self._baseline_suffix_list:
    295             baseline_name = _baseline_name(self._tool.filesystem, test_name, suffix)
    296             results_by_directory = self._baseline_optimizer.read_results_by_directory(baseline_name)
    297             if results_by_directory:
    298                 self._write("%s:" % baseline_name)
    299                 self._baseline_optimizer.write_by_directory(results_by_directory, self._write, "  ")
    300             elif options.missing:
    301                 self._write("%s: (no baselines found)" % baseline_name)
    302 
    303     def execute(self, options, args, tool):
    304         self._baseline_suffix_list = options.suffixes.split(',')
    305         port_names = tool.port_factory.all_port_names(options.platform)
    306         if not port_names:
    307             print "No port names match '%s'" % options.platform
    308             return
    309         self._port = tool.port_factory.get(port_names[0])
    310         self._baseline_optimizer = self._optimizer_class(tool, self._port, port_names, skip_scm_commands=False)
    311         for test_name in self._port.tests(args):
    312             self._analyze_baseline(options, test_name)
    313 
    314 
    315 class AbstractParallelRebaselineCommand(AbstractRebaseliningCommand):
    316     # not overriding execute() - pylint: disable=W0223
    317 
    318     def __init__(self, options=None):
    319         super(AbstractParallelRebaselineCommand, self).__init__(options=options)
    320         self._builder_data = {}
    321 
    322     def builder_data(self):
    323         if not self._builder_data:
    324             for builder_name in self._release_builders():
    325                 builder = self._tool.buildbot_for_builder_name(builder_name).builder_with_name(builder_name)
    326                 self._builder_data[builder_name] = builder.latest_layout_test_results()
    327         return self._builder_data
    328 
    329     # The release builders cycle much faster than the debug ones and cover all the platforms.
    330     def _release_builders(self):
    331         release_builders = []
    332         for builder_name in builders.all_builder_names():
    333             if builder_name.find('ASAN') != -1:
    334                 continue
    335             port = self._tool.port_factory.get_from_builder_name(builder_name)
    336             if port.test_configuration().build_type == 'release':
    337                 release_builders.append(builder_name)
    338         return release_builders
    339 
    340     def _run_webkit_patch(self, args, verbose):
    341         try:
    342             verbose_args = ['--verbose'] if verbose else []
    343             stderr = self._tool.executive.run_command([self._tool.path()] + verbose_args + args, cwd=self._tool.scm().checkout_root, return_stderr=True)
    344             for line in stderr.splitlines():
    345                 _log.warning(line)
    346         except ScriptError, e:
    347             _log.error(e)
    348 
    349     def _builders_to_fetch_from(self, builders_to_check):
    350         # This routine returns the subset of builders that will cover all of the baseline search paths
    351         # used in the input list. In particular, if the input list contains both Release and Debug
    352         # versions of a configuration, we *only* return the Release version (since we don't save
    353         # debug versions of baselines).
    354         release_builders = set()
    355         debug_builders = set()
    356         builders_to_fallback_paths = {}
    357         for builder in builders_to_check:
    358             port = self._tool.port_factory.get_from_builder_name(builder)
    359             if port.test_configuration().build_type == 'release':
    360                 release_builders.add(builder)
    361             else:
    362                 debug_builders.add(builder)
    363         for builder in list(release_builders) + list(debug_builders):
    364             port = self._tool.port_factory.get_from_builder_name(builder)
    365             fallback_path = port.baseline_search_path()
    366             if fallback_path not in builders_to_fallback_paths.values():
    367                 builders_to_fallback_paths[builder] = fallback_path
    368         return builders_to_fallback_paths.keys()
    369 
    370     def _rebaseline_commands(self, test_prefix_list, options):
    371         path_to_webkit_patch = self._tool.path()
    372         cwd = self._tool.scm().checkout_root
    373         copy_baseline_commands = []
    374         rebaseline_commands = []
    375         lines_to_remove = {}
    376         port = self._tool.port_factory.get()
    377 
    378         for test_prefix in test_prefix_list:
    379             for test in port.tests([test_prefix]):
    380                 for builder in self._builders_to_fetch_from(test_prefix_list[test_prefix]):
    381                     actual_failures_suffixes = self._suffixes_for_actual_failures(test, builder, test_prefix_list[test_prefix][builder])
    382                     if not actual_failures_suffixes:
    383                         # If we're not going to rebaseline the test because it's passing on this
    384                         # builder, we still want to remove the line from TestExpectations.
    385                         if test not in lines_to_remove:
    386                             lines_to_remove[test] = []
    387                         lines_to_remove[test].append(builder)
    388                         continue
    389 
    390                     suffixes = ','.join(actual_failures_suffixes)
    391                     cmd_line = ['--suffixes', suffixes, '--builder', builder, '--test', test]
    392                     if options.results_directory:
    393                         cmd_line.extend(['--results-directory', options.results_directory])
    394                     if options.verbose:
    395                         cmd_line.append('--verbose')
    396                     copy_baseline_commands.append(tuple([[self._tool.executable, path_to_webkit_patch, 'copy-existing-baselines-internal'] + cmd_line, cwd]))
    397                     rebaseline_commands.append(tuple([[self._tool.executable, path_to_webkit_patch, 'rebaseline-test-internal'] + cmd_line, cwd]))
    398         return copy_baseline_commands, rebaseline_commands, lines_to_remove
    399 
    400     def _serial_commands(self, command_results):
    401         files_to_add = set()
    402         files_to_delete = set()
    403         lines_to_remove = {}
    404         for output in [result[1].split('\n') for result in command_results]:
    405             file_added = False
    406             for line in output:
    407                 try:
    408                     if line:
    409                         parsed_line = json.loads(line)
    410                         if 'add' in parsed_line:
    411                             files_to_add.update(parsed_line['add'])
    412                         if 'delete' in parsed_line:
    413                             files_to_delete.update(parsed_line['delete'])
    414                         if 'remove-lines' in parsed_line:
    415                             for line_to_remove in parsed_line['remove-lines']:
    416                                 test = line_to_remove['test']
    417                                 builder = line_to_remove['builder']
    418                                 if test not in lines_to_remove:
    419                                     lines_to_remove[test] = []
    420                                 lines_to_remove[test].append(builder)
    421                         file_added = True
    422                 except ValueError:
    423                     _log.debug('"%s" is not a JSON object, ignoring' % line)
    424 
    425             if not file_added:
    426                 _log.debug('Could not add file based off output "%s"' % output)
    427 
    428         return list(files_to_add), list(files_to_delete), lines_to_remove
    429 
    430     def _optimize_baselines(self, test_prefix_list, verbose=False):
    431         optimize_commands = []
    432         for test in test_prefix_list:
    433             all_suffixes = set()
    434             for builder in self._builders_to_fetch_from(test_prefix_list[test]):
    435                 all_suffixes.update(self._suffixes_for_actual_failures(test, builder, test_prefix_list[test][builder]))
    436 
    437             # FIXME: We should propagate the platform options as well.
    438             cmd_line = ['--no-modify-scm', '--suffixes', ','.join(all_suffixes), test]
    439             if verbose:
    440                 cmd_line.append('--verbose')
    441 
    442             path_to_webkit_patch = self._tool.path()
    443             cwd = self._tool.scm().checkout_root
    444             optimize_commands.append(tuple([[self._tool.executable, path_to_webkit_patch, 'optimize-baselines'] + cmd_line, cwd]))
    445         return optimize_commands
    446 
    447     def _update_expectations_files(self, lines_to_remove):
    448         # FIXME: This routine is way too expensive. We're creating O(n ports) TestExpectations objects.
    449         # This is slow and uses a lot of memory.
    450         tests = lines_to_remove.keys()
    451         to_remove = []
    452 
    453         # This is so we remove lines for builders that skip this test, e.g. Android skips most
    454         # tests and we don't want to leave stray [ Android ] lines in TestExpectations..
    455         # This is only necessary for "webkit-patch rebaseline" and for rebaselining expected
    456         # failures from garden-o-matic. rebaseline-expectations and auto-rebaseline will always
    457         # pass the exact set of ports to rebaseline.
    458         for port_name in self._tool.port_factory.all_port_names():
    459             port = self._tool.port_factory.get(port_name)
    460             generic_expectations = TestExpectations(port, tests=tests, include_overrides=False)
    461             full_expectations = TestExpectations(port, tests=tests, include_overrides=True)
    462             for test in tests:
    463                 if self._port_skips_test(port, test, generic_expectations, full_expectations):
    464                     for test_configuration in port.all_test_configurations():
    465                         if test_configuration.version == port.test_configuration().version:
    466                             to_remove.append((test, test_configuration))
    467 
    468         for test in lines_to_remove:
    469             for builder in lines_to_remove[test]:
    470                 port = self._tool.port_factory.get_from_builder_name(builder)
    471                 for test_configuration in port.all_test_configurations():
    472                     if test_configuration.version == port.test_configuration().version:
    473                         to_remove.append((test, test_configuration))
    474 
    475         port = self._tool.port_factory.get()
    476         expectations = TestExpectations(port, include_overrides=False)
    477         expectationsString = expectations.remove_configurations(to_remove)
    478         path = port.path_to_generic_test_expectations_file()
    479         self._tool.filesystem.write_text_file(path, expectationsString)
    480 
    481     def _port_skips_test(self, port, test, generic_expectations, full_expectations):
    482         fs = port.host.filesystem
    483         if port.default_smoke_test_only():
    484             smoke_test_filename = fs.join(port.layout_tests_dir(), 'SmokeTests')
    485             if fs.exists(smoke_test_filename) and test not in fs.read_text_file(smoke_test_filename):
    486                 return True
    487 
    488         return (SKIP in full_expectations.get_expectations(test) and
    489                 SKIP not in generic_expectations.get_expectations(test))
    490 
    491     def _run_in_parallel_and_update_scm(self, commands):
    492         command_results = self._tool.executive.run_in_parallel(commands)
    493         log_output = '\n'.join(result[2] for result in command_results).replace('\n\n', '\n')
    494         for line in log_output.split('\n'):
    495             if line:
    496                 print >> sys.stderr, line  # FIXME: Figure out how to log properly.
    497 
    498         files_to_add, files_to_delete, lines_to_remove = self._serial_commands(command_results)
    499         if files_to_delete:
    500             self._tool.scm().delete_list(files_to_delete)
    501         if files_to_add:
    502             self._tool.scm().add_list(files_to_add)
    503         return lines_to_remove
    504 
    505     def _rebaseline(self, options, test_prefix_list):
    506         for test, builders_to_check in sorted(test_prefix_list.items()):
    507             _log.info("Rebaselining %s" % test)
    508             for builder, suffixes in sorted(builders_to_check.items()):
    509                 _log.debug("  %s: %s" % (builder, ",".join(suffixes)))
    510 
    511         copy_baseline_commands, rebaseline_commands, extra_lines_to_remove = self._rebaseline_commands(test_prefix_list, options)
    512         lines_to_remove = {}
    513 
    514         if copy_baseline_commands:
    515             self._run_in_parallel_and_update_scm(copy_baseline_commands)
    516         if rebaseline_commands:
    517             lines_to_remove = self._run_in_parallel_and_update_scm(rebaseline_commands)
    518 
    519         for test in extra_lines_to_remove:
    520             if test in lines_to_remove:
    521                 lines_to_remove[test] = lines_to_remove[test] + extra_lines_to_remove[test]
    522             else:
    523                 lines_to_remove[test] = extra_lines_to_remove[test]
    524 
    525         if lines_to_remove:
    526             self._update_expectations_files(lines_to_remove)
    527 
    528         if options.optimize:
    529             self._run_in_parallel_and_update_scm(self._optimize_baselines(test_prefix_list, options.verbose))
    530 
    531     def _suffixes_for_actual_failures(self, test, builder_name, existing_suffixes):
    532         actual_results = self.builder_data()[builder_name].actual_results(test)
    533         if not actual_results:
    534             return set()
    535         return set(existing_suffixes) & TestExpectations.suffixes_for_actual_expectations_string(actual_results)
    536 
    537 
    538 class RebaselineJson(AbstractParallelRebaselineCommand):
    539     name = "rebaseline-json"
    540     help_text = "Rebaseline based off JSON passed to stdin. Intended to only be called from other scripts."
    541 
    542     def __init__(self,):
    543         super(RebaselineJson, self).__init__(options=[
    544             self.no_optimize_option,
    545             self.results_directory_option,
    546             ])
    547 
    548     def execute(self, options, args, tool):
    549         self._rebaseline(options, json.loads(sys.stdin.read()))
    550 
    551 
    552 class RebaselineExpectations(AbstractParallelRebaselineCommand):
    553     name = "rebaseline-expectations"
    554     help_text = "Rebaselines the tests indicated in TestExpectations."
    555     show_in_main_help = True
    556 
    557     def __init__(self):
    558         super(RebaselineExpectations, self).__init__(options=[
    559             self.no_optimize_option,
    560             ] + self.platform_options)
    561         self._test_prefix_list = None
    562 
    563     def _tests_to_rebaseline(self, port):
    564         tests_to_rebaseline = {}
    565         for path, value in port.expectations_dict().items():
    566             expectations = TestExpectations(port, include_overrides=False, expectations_dict={path: value})
    567             for test in expectations.get_rebaselining_failures():
    568                 suffixes = TestExpectations.suffixes_for_expectations(expectations.get_expectations(test))
    569                 tests_to_rebaseline[test] = suffixes or BASELINE_SUFFIX_LIST
    570         return tests_to_rebaseline
    571 
    572     def _add_tests_to_rebaseline_for_port(self, port_name):
    573         builder_name = builders.builder_name_for_port_name(port_name)
    574         if not builder_name:
    575             return
    576         tests = self._tests_to_rebaseline(self._tool.port_factory.get(port_name)).items()
    577 
    578         if tests:
    579             _log.info("Retrieving results for %s from %s." % (port_name, builder_name))
    580 
    581         for test_name, suffixes in tests:
    582             _log.info("    %s (%s)" % (test_name, ','.join(suffixes)))
    583             if test_name not in self._test_prefix_list:
    584                 self._test_prefix_list[test_name] = {}
    585             self._test_prefix_list[test_name][builder_name] = suffixes
    586 
    587     def execute(self, options, args, tool):
    588         options.results_directory = None
    589         self._test_prefix_list = {}
    590         port_names = tool.port_factory.all_port_names(options.platform)
    591         for port_name in port_names:
    592             self._add_tests_to_rebaseline_for_port(port_name)
    593         if not self._test_prefix_list:
    594             _log.warning("Did not find any tests marked Rebaseline.")
    595             return
    596 
    597         self._rebaseline(options, self._test_prefix_list)
    598 
    599 
    600 class Rebaseline(AbstractParallelRebaselineCommand):
    601     name = "rebaseline"
    602     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."
    603     show_in_main_help = True
    604     argument_names = "[TEST_NAMES]"
    605 
    606     def __init__(self):
    607         super(Rebaseline, self).__init__(options=[
    608             self.no_optimize_option,
    609             # FIXME: should we support the platform options in addition to (or instead of) --builders?
    610             self.suffixes_option,
    611             self.results_directory_option,
    612             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)"),
    613             ])
    614 
    615     def _builders_to_pull_from(self):
    616         chosen_names = self._tool.user.prompt_with_list("Which builder to pull results from:", self._release_builders(), can_choose_multiple=True)
    617         return [self._builder_with_name(name) for name in chosen_names]
    618 
    619     def _builder_with_name(self, name):
    620         return self._tool.buildbot_for_builder_name(name).builder_with_name(name)
    621 
    622     def execute(self, options, args, tool):
    623         if not args:
    624             _log.error("Must list tests to rebaseline.")
    625             return
    626 
    627         if options.builders:
    628             builders_to_check = []
    629             for builder_names in options.builders:
    630                 builders_to_check += [self._builder_with_name(name) for name in builder_names.split(",")]
    631         else:
    632             builders_to_check = self._builders_to_pull_from()
    633 
    634         test_prefix_list = {}
    635         suffixes_to_update = options.suffixes.split(",")
    636 
    637         for builder in builders_to_check:
    638             for test in args:
    639                 if test not in test_prefix_list:
    640                     test_prefix_list[test] = {}
    641                 test_prefix_list[test][builder.name()] = suffixes_to_update
    642 
    643         if options.verbose:
    644             _log.debug("rebaseline-json: " + str(test_prefix_list))
    645 
    646         self._rebaseline(options, test_prefix_list)
    647 
    648 
    649 class AutoRebaseline(AbstractParallelRebaselineCommand):
    650     name = "auto-rebaseline"
    651     help_text = "Rebaselines any NeedsRebaseline lines in TestExpectations that have cycled through all the bots."
    652     AUTO_REBASELINE_BRANCH_NAME = "auto-rebaseline-temporary-branch"
    653 
    654     # Rietveld uploader stinks. Limit the number of rebaselines in a given patch to keep upload from failing.
    655     # FIXME: http://crbug.com/263676 Obviously we should fix the uploader here.
    656     MAX_LINES_TO_REBASELINE = 200
    657 
    658     SECONDS_BEFORE_GIVING_UP = 300
    659 
    660     def __init__(self):
    661         super(AutoRebaseline, self).__init__(options=[
    662             # FIXME: Remove this option.
    663             self.no_optimize_option,
    664             # FIXME: Remove this option.
    665             self.results_directory_option,
    666             ])
    667 
    668     def bot_revision_data(self):
    669         revisions = []
    670         for result in self.builder_data().values():
    671             if result.run_was_interrupted():
    672                 _log.error("Can't rebaseline because the latest run on %s exited early." % result.builder_name())
    673                 return []
    674             revisions.append({
    675                 "builder": result.builder_name(),
    676                 "revision": result.blink_revision(),
    677             })
    678         return revisions
    679 
    680     def tests_to_rebaseline(self, tool, min_revision, print_revisions):
    681         port = tool.port_factory.get()
    682         expectations_file_path = port.path_to_generic_test_expectations_file()
    683 
    684         tests = set()
    685         revision = None
    686         author = None
    687         bugs = set()
    688         has_any_needs_rebaseline_lines = False
    689 
    690         for line in tool.scm().blame(expectations_file_path).split("\n"):
    691             comment_index = line.find("#")
    692             if comment_index == -1:
    693                 comment_index = len(line)
    694             line_without_comments = re.sub(r"\s+", " ", line[:comment_index].strip())
    695 
    696             if "NeedsRebaseline" not in line_without_comments:
    697                 continue
    698 
    699             has_any_needs_rebaseline_lines = True
    700 
    701             parsed_line = re.match("^(\S*)[^(]*\((\S*).*?([^ ]*)\ \[[^[]*$", line_without_comments)
    702 
    703             commit_hash = parsed_line.group(1)
    704             svn_revision = tool.scm().svn_revision_from_git_commit(commit_hash)
    705 
    706             test = parsed_line.group(3)
    707             if print_revisions:
    708                 _log.info("%s is waiting for r%s" % (test, svn_revision))
    709 
    710             if not svn_revision or svn_revision > min_revision:
    711                 continue
    712 
    713             if revision and svn_revision != revision:
    714                 continue
    715 
    716             if not revision:
    717                 revision = svn_revision
    718                 author = parsed_line.group(2)
    719 
    720             bugs.update(re.findall("crbug\.com\/(\d+)", line_without_comments))
    721             tests.add(test)
    722 
    723             if len(tests) >= self.MAX_LINES_TO_REBASELINE:
    724                 _log.info("Too many tests to rebaseline in one patch. Doing the first %d." % self.MAX_LINES_TO_REBASELINE)
    725                 break
    726 
    727         return tests, revision, author, bugs, has_any_needs_rebaseline_lines
    728 
    729     def link_to_patch(self, revision):
    730         return "http://src.chromium.org/viewvc/blink?view=revision&revision=" + str(revision)
    731 
    732     def commit_message(self, author, revision, bugs):
    733         bug_string = ""
    734         if bugs:
    735             bug_string = "BUG=%s\n" % ",".join(bugs)
    736 
    737         return """Auto-rebaseline for r%s
    738 
    739 %s
    740 
    741 %sTBR=%s
    742 """ % (revision, self.link_to_patch(revision), bug_string, author)
    743 
    744     def get_test_prefix_list(self, tests):
    745         test_prefix_list = {}
    746         lines_to_remove = {}
    747 
    748         for builder_name in self._release_builders():
    749             port_name = builders.port_name_for_builder_name(builder_name)
    750             port = self._tool.port_factory.get(port_name)
    751             expectations = TestExpectations(port, include_overrides=True)
    752             for test in expectations.get_needs_rebaseline_failures():
    753                 if test not in tests:
    754                     continue
    755 
    756                 if test not in test_prefix_list:
    757                     lines_to_remove[test] = []
    758                     test_prefix_list[test] = {}
    759                 lines_to_remove[test].append(builder_name)
    760                 test_prefix_list[test][builder_name] = BASELINE_SUFFIX_LIST
    761 
    762         return test_prefix_list, lines_to_remove
    763 
    764     def _run_git_cl_command(self, options, command):
    765         subprocess_command = ['git', 'cl'] + command
    766         if options.verbose:
    767             subprocess_command.append('--verbose')
    768 
    769         process = self._tool.executive.popen(subprocess_command, stdout=self._tool.executive.PIPE, stderr=self._tool.executive.STDOUT)
    770         last_output_time = time.time()
    771 
    772         # git cl sometimes completely hangs. Bail if we haven't gotten any output to stdout/stderr in a while.
    773         while process.poll() == None and time.time() < last_output_time + self.SECONDS_BEFORE_GIVING_UP:
    774             # FIXME: This doesn't make any sense. readline blocks, so all this code to
    775             # try and bail is useless. Instead, we should do the readline calls on a
    776             # subthread. Then the rest of this code would make sense.
    777             out = process.stdout.readline().rstrip('\n')
    778             if out:
    779                 last_output_time = time.time()
    780                 _log.info(out)
    781 
    782         if process.poll() == None:
    783             _log.error('Command hung: %s' % subprocess_command)
    784             return False
    785         return True
    786 
    787     # FIXME: Move this somewhere more general.
    788     def tree_status(self):
    789         blink_tree_status_url = "http://blink-status.appspot.com/status"
    790         status = urllib2.urlopen(blink_tree_status_url).read().lower()
    791         if status.find('closed') != -1 or status == "0":
    792             return 'closed'
    793         elif status.find('open') != -1 or status == "1":
    794             return 'open'
    795         return 'unknown'
    796 
    797     def execute(self, options, args, tool):
    798         if tool.scm().executable_name == "svn":
    799             _log.error("Auto rebaseline only works with a git checkout.")
    800             return
    801 
    802         if tool.scm().has_working_directory_changes():
    803             _log.error("Cannot proceed with working directory changes. Clean working directory first.")
    804             return
    805 
    806         revision_data = self.bot_revision_data()
    807         if not revision_data:
    808             return
    809 
    810         min_revision = int(min([item["revision"] for item in revision_data]))
    811         tests, revision, author, bugs, has_any_needs_rebaseline_lines = self.tests_to_rebaseline(tool, min_revision, print_revisions=options.verbose)
    812 
    813         if options.verbose:
    814             _log.info("Min revision across all bots is %s." % min_revision)
    815             for item in revision_data:
    816                 _log.info("%s: r%s" % (item["builder"], item["revision"]))
    817 
    818         if not tests:
    819             _log.debug('No tests to rebaseline.')
    820             return
    821 
    822         if self.tree_status() == 'closed':
    823             _log.info('Cannot proceed. Tree is closed.')
    824             return
    825 
    826         _log.info('Rebaselining %s for r%s by %s.' % (list(tests), revision, author))
    827 
    828         test_prefix_list, lines_to_remove = self.get_test_prefix_list(tests)
    829 
    830         did_finish = False
    831         try:
    832             old_branch_name = tool.scm().current_branch()
    833             tool.scm().delete_branch(self.AUTO_REBASELINE_BRANCH_NAME)
    834             tool.scm().create_clean_branch(self.AUTO_REBASELINE_BRANCH_NAME)
    835 
    836             # If the tests are passing everywhere, then this list will be empty. We don't need
    837             # to rebaseline, but we'll still need to update TestExpectations.
    838             if test_prefix_list:
    839                 self._rebaseline(options, test_prefix_list)
    840 
    841             tool.scm().commit_locally_with_message(self.commit_message(author, revision, bugs))
    842 
    843             # FIXME: It would be nice if we could dcommit the patch without uploading, but still
    844             # go through all the precommit hooks. For rebaselines with lots of files, uploading
    845             # takes a long time and sometimes fails, but we don't want to commit if, e.g. the
    846             # tree is closed.
    847             did_finish = self._run_git_cl_command(options, ['upload', '-f'])
    848 
    849             if did_finish:
    850                 # Uploading can take a very long time. Do another pull to make sure TestExpectations is up to date,
    851                 # so the dcommit can go through.
    852                 # FIXME: Log the pull and dcommit stdout/stderr to the log-server.
    853                 tool.executive.run_command(['git', 'pull'])
    854 
    855                 self._run_git_cl_command(options, ['dcommit', '-f'])
    856         except Exception as e:
    857             _log.error(e)
    858         finally:
    859             if did_finish:
    860                 self._run_git_cl_command(options, ['set_close'])
    861             tool.scm().ensure_cleanly_tracking_remote_master()
    862             tool.scm().checkout_branch(old_branch_name)
    863             tool.scm().delete_branch(self.AUTO_REBASELINE_BRANCH_NAME)
    864 
    865 
    866 class RebaselineOMatic(AbstractDeclarativeCommand):
    867     name = "rebaseline-o-matic"
    868     help_text = "Calls webkit-patch auto-rebaseline in a loop."
    869     show_in_main_help = True
    870 
    871     SLEEP_TIME_IN_SECONDS = 30
    872     LOG_SERVER = 'blinkrebaseline.appspot.com'
    873     QUIT_LOG = '##QUIT##'
    874 
    875     # Uploaded log entries append to the existing entry unless the
    876     # newentry flag is set. In that case it starts a new entry to
    877     # start appending to.
    878     def _log_to_server(self, log='', is_new_entry=False):
    879         query = {
    880             'log': log,
    881         }
    882         if is_new_entry:
    883             query['newentry'] = 'on'
    884         try:
    885             urllib2.urlopen("http://" + self.LOG_SERVER + "/updatelog", data=urllib.urlencode(query))
    886         except:
    887             traceback.print_exc(file=sys.stderr)
    888 
    889     def _log_to_server_thread(self):
    890         is_new_entry = True
    891         while True:
    892             messages = [self._log_queue.get()]
    893             while not self._log_queue.empty():
    894                 messages.append(self._log_queue.get())
    895             self._log_to_server('\n'.join(messages), is_new_entry=is_new_entry)
    896             is_new_entry = False
    897             if self.QUIT_LOG in messages:
    898                 return
    899 
    900     def _post_log_to_server(self, log):
    901         self._log_queue.put(log)
    902 
    903     def _log_line(self, handle):
    904         out = handle.readline().rstrip('\n')
    905         if out:
    906             if self._verbose:
    907                 print out
    908             self._post_log_to_server(out)
    909         return out
    910 
    911     def _run_logged_command(self, command):
    912         process = self._tool.executive.popen(command, stdout=self._tool.executive.PIPE, stderr=self._tool.executive.STDOUT)
    913 
    914         out = self._log_line(process.stdout)
    915         while out:
    916             # FIXME: This should probably batch up lines if they're available and log to the server once.
    917             out = self._log_line(process.stdout)
    918 
    919     def _do_one_rebaseline(self):
    920         self._log_queue = Queue.Queue(256)
    921         log_thread = threading.Thread(name='LogToServer', target=self._log_to_server_thread)
    922         log_thread.start()
    923         try:
    924             old_branch_name = self._tool.scm().current_branch()
    925             self._run_logged_command(['git', 'pull'])
    926             rebaseline_command = [self._tool.filesystem.join(self._tool.scm().checkout_root, 'Tools', 'Scripts', 'webkit-patch'), 'auto-rebaseline']
    927             if self._verbose:
    928                 rebaseline_command.append('--verbose')
    929             self._run_logged_command(rebaseline_command)
    930         except:
    931             self._log_queue.put(self.QUIT_LOG)
    932             traceback.print_exc(file=sys.stderr)
    933             # Sometimes git crashes and leaves us on a detached head.
    934             self._tool.scm().checkout_branch(old_branch_name)
    935         else:
    936             self._log_queue.put(self.QUIT_LOG)
    937         log_thread.join()
    938 
    939     def execute(self, options, args, tool):
    940         self._verbose = options.verbose
    941         while True:
    942             self._do_one_rebaseline()
    943             time.sleep(self.SLEEP_TIME_IN_SECONDS)
    944