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