1#!/usr/bin/env python
2# Copyright (C) 2010 Google Inc. All rights reserved.
3#
4# Redistribution and use in source and binary forms, with or without
5# modification, are permitted provided that the following conditions are
6# met:
7#
8#     * Redistributions of source code must retain the above copyright
9# notice, this list of conditions and the following disclaimer.
10#     * Redistributions in binary form must reproduce the above
11# copyright notice, this list of conditions and the following disclaimer
12# in the documentation and/or other materials provided with the
13# distribution.
14#     * Neither the name of Google Inc. nor the names of its
15# contributors may be used to endorse or promote products derived from
16# this software without specific prior written permission.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30"""Rebaselining tool that automatically produces baselines for all platforms.
31
32The script does the following for each platform specified:
33  1. Compile a list of tests that need rebaselining.
34  2. Download test result archive from buildbot for the platform.
35  3. Extract baselines from the archive file for all identified files.
36  4. Add new baselines to SVN repository.
37  5. For each test that has been rebaselined, remove this platform option from
38     the test in test_expectation.txt. If no other platforms remain after
39     removal, delete the rebaselined test from the file.
40
41At the end, the script generates a html that compares old and new baselines.
42"""
43
44from __future__ import with_statement
45
46import copy
47import logging
48import optparse
49import re
50import sys
51import time
52
53from webkitpy.common.checkout import scm
54from webkitpy.common.system import zipfileset
55from webkitpy.common.system import path
56from webkitpy.common.system import urlfetcher
57from webkitpy.common.system.executive import ScriptError
58
59from webkitpy.layout_tests import port
60from webkitpy.layout_tests import read_checksum_from_png
61from webkitpy.layout_tests.layout_package import test_expectations
62
63_log = logging.getLogger(__name__)
64
65BASELINE_SUFFIXES = ('.txt', '.png', '.checksum')
66
67ARCHIVE_DIR_NAME_DICT = {
68    'chromium-win-win7': 'Webkit_Win7',
69    'chromium-win-vista': 'Webkit_Vista',
70    'chromium-win-xp': 'Webkit_Win',
71    'chromium-mac-leopard': 'Webkit_Mac10_5',
72    'chromium-mac-snowleopard': 'Webkit_Mac10_6',
73    'chromium-linux-x86': 'Webkit_Linux',
74    'chromium-linux-x86_64': 'Webkit_Linux_64',
75    'chromium-gpu-mac-snowleopard': 'Webkit_Mac10_6_-_GPU',
76    'chromium-gpu-win-xp': 'Webkit_Win_-_GPU',
77    'chromium-gpu-win-win7': 'Webkit_Win7_-_GPU',
78    'chromium-gpu-linux': 'Webkit_Linux_-_GPU',
79    'chromium-gpu-linux-x86_64': 'Webkit_Linux_64_-_GPU',
80}
81
82
83def log_dashed_string(text, platform, logging_level=logging.INFO):
84    """Log text message with dashes on both sides."""
85
86    msg = text
87    if platform:
88        msg += ': ' + platform
89    if len(msg) < 78:
90        dashes = '-' * ((78 - len(msg)) / 2)
91        msg = '%s %s %s' % (dashes, msg, dashes)
92
93    if logging_level == logging.ERROR:
94        _log.error(msg)
95    elif logging_level == logging.WARNING:
96        _log.warn(msg)
97    else:
98        _log.info(msg)
99
100
101def setup_html_directory(filesystem, parent_directory):
102    """Setup the directory to store html results.
103
104       All html related files are stored in the "rebaseline_html" subdirectory of
105       the parent directory. The path to the created directory is returned.
106    """
107
108    if not parent_directory:
109        parent_directory = str(filesystem.mkdtemp())
110    else:
111        filesystem.maybe_make_directory(parent_directory)
112
113    html_directory = filesystem.join(parent_directory, 'rebaseline_html')
114    _log.info('Html directory: "%s"', html_directory)
115
116    if filesystem.exists(html_directory):
117        filesystem.rmtree(html_directory)
118        _log.info('Deleted html directory: "%s"', html_directory)
119
120    filesystem.maybe_make_directory(html_directory)
121    return html_directory
122
123
124def get_result_file_fullpath(filesystem, html_directory, baseline_filename, platform,
125                             result_type):
126    """Get full path of the baseline result file.
127
128    Args:
129      filesystem: wrapper object
130      html_directory: directory that stores the html related files.
131      baseline_filename: name of the baseline file.
132      platform: win, linux or mac
133      result_type: type of the baseline result: '.txt', '.png'.
134
135    Returns:
136      Full path of the baseline file for rebaselining result comparison.
137    """
138
139    base, ext = filesystem.splitext(baseline_filename)
140    result_filename = '%s-%s-%s%s' % (base, platform, result_type, ext)
141    fullpath = filesystem.join(html_directory, result_filename)
142    _log.debug('  Result file full path: "%s".', fullpath)
143    return fullpath
144
145
146class Rebaseliner(object):
147    """Class to produce new baselines for a given platform."""
148
149    REVISION_REGEX = r'<a href=\"(\d+)/\">'
150
151    def __init__(self, running_port, target_port, platform, options, url_fetcher, zip_factory, scm):
152        """
153        Args:
154            running_port: the Port the script is running on.
155            target_port: the Port the script uses to find port-specific
156                configuration information like the test_expectations.txt
157                file location and the list of test platforms.
158            platform: the test platform to rebaseline
159            options: the command-line options object.
160            url_fetcher: object that can fetch objects from URLs
161            zip_factory: optional object that can fetch zip files from URLs
162            scm: scm object for adding new baselines
163        """
164        self._platform = platform
165        self._options = options
166        self._port = running_port
167        self._filesystem = running_port._filesystem
168        self._target_port = target_port
169
170        self._rebaseline_port = port.get(platform, options, filesystem=self._filesystem)
171        self._rebaselining_tests = set()
172        self._rebaselined_tests = []
173
174        # Create tests and expectations helper which is used to:
175        #   -. compile list of tests that need rebaselining.
176        #   -. update the tests in test_expectations file after rebaseline
177        #      is done.
178        expectations_str = self._rebaseline_port.test_expectations()
179        self._test_expectations = test_expectations.TestExpectations(
180            self._rebaseline_port, None, expectations_str, self._rebaseline_port.test_configuration(), False)
181        self._url_fetcher = url_fetcher
182        self._zip_factory = zip_factory
183        self._scm = scm
184
185    def run(self):
186        """Run rebaseline process."""
187
188        log_dashed_string('Compiling rebaselining tests', self._platform)
189        if not self._compile_rebaselining_tests():
190            return False
191        if not self._rebaselining_tests:
192            return True
193
194        log_dashed_string('Downloading archive', self._platform)
195        archive_file = self._download_buildbot_archive()
196        _log.info('')
197        if not archive_file:
198            _log.error('No archive found.')
199            return False
200
201        log_dashed_string('Extracting and adding new baselines', self._platform)
202        if not self._extract_and_add_new_baselines(archive_file):
203            archive_file.close()
204            return False
205
206        archive_file.close()
207
208        log_dashed_string('Updating rebaselined tests in file', self._platform)
209
210        if len(self._rebaselining_tests) != len(self._rebaselined_tests):
211            _log.warning('NOT ALL TESTS THAT NEED REBASELINING HAVE BEEN REBASELINED.')
212            _log.warning('  Total tests needing rebaselining: %d', len(self._rebaselining_tests))
213            _log.warning('  Total tests rebaselined: %d', len(self._rebaselined_tests))
214            return False
215
216        _log.warning('All tests needing rebaselining were successfully rebaselined.')
217
218        return True
219
220    def remove_rebaselining_expectations(self, tests, backup):
221        """if backup is True, we backup the original test expectations file."""
222        new_expectations = self._test_expectations.remove_rebaselined_tests(tests)
223        path = self._target_port.path_to_test_expectations_file()
224        if backup:
225            date_suffix = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
226            backup_file = '%s.orig.%s' % (path, date_suffix)
227            if self._filesystem.exists(backup_file):
228                self._filesystem.remove(backup_file)
229            _log.info('Saving original file to "%s"', backup_file)
230            self._filesystem.move(path, backup_file)
231
232        self._filesystem.write_text_file(path, new_expectations)
233        # self._scm.add(path)
234
235    def get_rebaselined_tests(self):
236        return self._rebaselined_tests
237
238    def _compile_rebaselining_tests(self):
239        """Compile list of tests that need rebaselining for the platform.
240
241        Returns:
242          False if reftests are wrongly marked as 'needs rebaselining' or True
243        """
244
245        self._rebaselining_tests = self._test_expectations.get_rebaselining_failures()
246        if not self._rebaselining_tests:
247            _log.warn('No tests found that need rebaselining.')
248            return True
249
250        fs = self._target_port._filesystem
251        for test in self._rebaselining_tests:
252            test_abspath = self._target_port.abspath_for_test(test)
253            if (fs.exists(self._target_port.reftest_expected_filename(test_abspath)) or
254                fs.exists(self._target_port.reftest_expected_mismatch_filename(test_abspath))):
255                _log.error('%s seems to be a reftest. We can not rebase for reftests.', test)
256                self._rebaselining_tests = set()
257                return False
258
259        _log.info('Total number of tests needing rebaselining for "%s": "%d"',
260                  self._platform, len(self._rebaselining_tests))
261
262        test_no = 1
263        for test in self._rebaselining_tests:
264            _log.info('  %d: %s', test_no, test)
265            test_no += 1
266
267        return True
268
269    def _get_latest_revision(self, url):
270        """Get the latest layout test revision number from buildbot.
271
272        Args:
273          url: Url to retrieve layout test revision numbers.
274
275        Returns:
276          latest revision or
277          None on failure.
278        """
279
280        _log.debug('Url to retrieve revision: "%s"', url)
281
282        content = self._url_fetcher.fetch(url)
283
284        revisions = re.findall(self.REVISION_REGEX, content)
285        if not revisions:
286            _log.error('Failed to find revision, content: "%s"', content)
287            return None
288
289        revisions.sort(key=int)
290        _log.info('Latest revision: "%s"', revisions[len(revisions) - 1])
291        return revisions[len(revisions) - 1]
292
293    def _get_archive_dir_name(self, platform):
294        """Get name of the layout test archive directory.
295
296        Returns:
297          Directory name or
298          None on failure
299        """
300
301        if platform in ARCHIVE_DIR_NAME_DICT:
302            return ARCHIVE_DIR_NAME_DICT[platform]
303        else:
304            _log.error('Cannot find platform key %s in archive '
305                       'directory name dictionary', platform)
306            return None
307
308    def _get_archive_url(self):
309        """Generate the url to download latest layout test archive.
310
311        Returns:
312          Url to download archive or
313          None on failure
314        """
315
316        if self._options.force_archive_url:
317            return self._options.force_archive_url
318
319        dir_name = self._get_archive_dir_name(self._platform)
320        if not dir_name:
321            return None
322
323        _log.debug('Buildbot platform dir name: "%s"', dir_name)
324
325        url_base = '%s/%s/' % (self._options.archive_url, dir_name)
326        latest_revision = self._get_latest_revision(url_base)
327        if latest_revision is None or latest_revision <= 0:
328            return None
329        archive_url = '%s%s/layout-test-results.zip' % (url_base, latest_revision)
330        _log.info('Archive url: "%s"', archive_url)
331        return archive_url
332
333    def _download_buildbot_archive(self):
334        """Download layout test archive file from buildbot and return a handle to it."""
335        url = self._get_archive_url()
336        if url is None:
337            return None
338
339        archive_file = zipfileset.ZipFileSet(url, filesystem=self._filesystem,
340                                             zip_factory=self._zip_factory)
341        _log.info('Archive downloaded')
342        return archive_file
343
344    def _extract_and_add_new_baselines(self, zip_file):
345        """Extract new baselines from the zip file and add them to SVN repository.
346
347        Returns:
348          List of tests that have been rebaselined or None on failure."""
349        zip_namelist = zip_file.namelist()
350
351        _log.debug('zip file namelist:')
352        for name in zip_namelist:
353            _log.debug('  ' + name)
354
355        _log.debug('Platform dir: "%s"', self._platform)
356
357        self._rebaselined_tests = []
358        for test_no, test in enumerate(self._rebaselining_tests):
359            _log.info('Test %d: %s', test_no + 1, test)
360            self._extract_and_add_new_baseline(test, zip_file)
361
362        zip_file.close()
363
364        return self._rebaselined_tests
365
366    def _extract_and_add_new_baseline(self, test, zip_file):
367        found = False
368        scm_error = False
369        test_basename = self._filesystem.splitext(test)[0]
370        for suffix in BASELINE_SUFFIXES:
371            archive_test_name = 'layout-test-results/%s-actual%s' % (test_basename, suffix)
372            _log.debug('  Archive test file name: "%s"', archive_test_name)
373            if not archive_test_name in zip_file.namelist():
374                _log.info('  %s file not in archive.', suffix)
375                continue
376
377            found = True
378            _log.info('  %s file found in archive.', suffix)
379
380            temp_name = self._extract_from_zip_to_tempfile(zip_file, archive_test_name)
381
382            expected_filename = '%s-expected%s' % (test_basename, suffix)
383            expected_fullpath = self._filesystem.join(
384                self._rebaseline_port.baseline_path(), expected_filename)
385            expected_fullpath = self._filesystem.normpath(expected_fullpath)
386            _log.debug('  Expected file full path: "%s"', expected_fullpath)
387
388            # TODO(victorw): for now, the rebaselining tool checks whether
389            # or not THIS baseline is duplicate and should be skipped.
390            # We could improve the tool to check all baselines in upper
391            # and lower levels and remove all duplicated baselines.
392            if self._is_dup_baseline(temp_name, expected_fullpath, test, suffix, self._platform):
393                self._filesystem.remove(temp_name)
394                self._delete_baseline(expected_fullpath)
395                continue
396
397            if suffix == '.checksum' and self._png_has_same_checksum(temp_name, test, expected_fullpath):
398                self._filesystem.remove(temp_name)
399                # If an old checksum exists, delete it.
400                self._delete_baseline(expected_fullpath)
401                continue
402
403            self._filesystem.maybe_make_directory(self._filesystem.dirname(expected_fullpath))
404            self._filesystem.move(temp_name, expected_fullpath)
405
406            if self._scm.add(expected_fullpath, return_exit_code=True):
407                # FIXME: print detailed diagnose messages
408                scm_error = True
409            elif suffix != '.checksum':
410                self._create_html_baseline_files(expected_fullpath)
411
412        if not found:
413            _log.warn('  No new baselines found in archive.')
414        elif scm_error:
415            _log.warn('  Failed to add baselines to your repository.')
416        else:
417            _log.info('  Rebaseline succeeded.')
418            self._rebaselined_tests.append(test)
419
420    def _extract_from_zip_to_tempfile(self, zip_file, filename):
421        """Extracts |filename| from |zip_file|, a ZipFileSet. Returns the full
422           path name to the extracted file."""
423        data = zip_file.read(filename)
424        suffix = self._filesystem.splitext(filename)[1]
425        tempfile, temp_name = self._filesystem.open_binary_tempfile(suffix)
426        tempfile.write(data)
427        tempfile.close()
428        return temp_name
429
430    def _png_has_same_checksum(self, checksum_path, test, checksum_expected_fullpath):
431        """Returns True if the fallback png for |checksum_expected_fullpath|
432        contains the same checksum."""
433        fs = self._filesystem
434        png_fullpath = self._first_fallback_png_for_test(test)
435
436        if not fs.exists(png_fullpath):
437            _log.error('  Checksum without png file found! Expected %s to exist.' % png_fullpath)
438            return False
439
440        with fs.open_binary_file_for_reading(png_fullpath) as filehandle:
441            checksum_in_png = read_checksum_from_png.read_checksum(filehandle)
442            checksum_in_text_file = fs.read_text_file(checksum_path)
443            if checksum_in_png and checksum_in_png != checksum_in_text_file:
444                _log.error("  checksum in %s and %s don't match!  Continuing"
445                           " to copy but please investigate." % (
446                           checksum_expected_fullpath, png_fullpath))
447            return checksum_in_text_file == checksum_in_png
448
449    def _first_fallback_png_for_test(self, test):
450        test_filepath = self._filesystem.join(self._target_port.layout_tests_dir(), test)
451        all_baselines = self._rebaseline_port.expected_baselines(
452            test_filepath, '.png', True)
453        return self._filesystem.join(all_baselines[0][0], all_baselines[0][1])
454
455    def _is_dup_baseline(self, new_baseline, baseline_path, test, suffix, platform):
456        """Check whether a baseline is duplicate and can fallback to same
457           baseline for another platform. For example, if a test has same
458           baseline on linux and windows, then we only store windows
459           baseline and linux baseline will fallback to the windows version.
460
461        Args:
462          new_baseline: temp filename containing the new baseline results
463          baseline_path: baseline expectation file name.
464          test: test name.
465          suffix: file suffix of the expected results, including dot;
466                  e.g. '.txt' or '.png'.
467          platform: baseline platform 'mac', 'win' or 'linux'.
468
469        Returns:
470          True if the baseline is unnecessary.
471          False otherwise.
472        """
473        test_filepath = self._filesystem.join(self._target_port.layout_tests_dir(), test)
474        all_baselines = self._rebaseline_port.expected_baselines(
475            test_filepath, suffix, True)
476
477        for fallback_dir, fallback_file in all_baselines:
478            if not fallback_dir or not fallback_file:
479                continue
480
481            fallback_fullpath = self._filesystem.normpath(
482                self._filesystem.join(fallback_dir, fallback_file))
483            if fallback_fullpath.lower() == baseline_path.lower():
484                continue
485
486            new_output = self._filesystem.read_binary_file(new_baseline)
487            fallback_output = self._filesystem.read_binary_file(fallback_fullpath)
488            is_image = baseline_path.lower().endswith('.png')
489            if not self._diff_baselines(new_output, fallback_output, is_image):
490                _log.info('  Found same baseline at %s', fallback_fullpath)
491                return True
492            return False
493
494        return False
495
496    def _diff_baselines(self, output1, output2, is_image):
497        """Check whether two baselines are different.
498
499        Args:
500          output1, output2: contents of the baselines to compare.
501
502        Returns:
503          True if two files are different or have different extensions.
504          False otherwise.
505        """
506
507        if is_image:
508            return self._port.diff_image(output1, output2, None)
509
510        return self._port.compare_text(output1, output2)
511
512    def _delete_baseline(self, filename):
513        """Remove the file from repository and delete it from disk.
514
515        Args:
516          filename: full path of the file to delete.
517        """
518
519        if not filename or not self._filesystem.isfile(filename):
520            return
521        self._scm.delete(filename)
522
523    def _create_html_baseline_files(self, baseline_fullpath):
524        """Create baseline files (old, new and diff) in html directory.
525
526           The files are used to compare the rebaselining results.
527
528        Args:
529          baseline_fullpath: full path of the expected baseline file.
530        """
531
532        if not baseline_fullpath or not self._filesystem.exists(baseline_fullpath):
533            return
534
535        # Copy the new baseline to html directory for result comparison.
536        baseline_filename = self._filesystem.basename(baseline_fullpath)
537        new_file = get_result_file_fullpath(self._filesystem, self._options.html_directory,
538                                            baseline_filename, self._platform, 'new')
539        self._filesystem.copyfile(baseline_fullpath, new_file)
540        _log.info('  Html: copied new baseline file from "%s" to "%s".',
541                  baseline_fullpath, new_file)
542
543        # Get the old baseline from the repository and save to the html directory.
544        try:
545            output = self._scm.show_head(baseline_fullpath)
546        except ScriptError, e:
547            _log.info(e)
548            output = ""
549
550        if (not output) or (output.upper().rstrip().endswith('NO SUCH FILE OR DIRECTORY')):
551            _log.info('  No base file: "%s"', baseline_fullpath)
552            return
553        base_file = get_result_file_fullpath(self._filesystem, self._options.html_directory,
554                                             baseline_filename, self._platform, 'old')
555        if base_file.upper().endswith('.PNG'):
556            self._filesystem.write_binary_file(base_file, output)
557        else:
558            self._filesystem.write_text_file(base_file, output)
559        _log.info('  Html: created old baseline file: "%s".', base_file)
560
561        # Get the diff between old and new baselines and save to the html dir.
562        if baseline_filename.upper().endswith('.TXT'):
563            output = self._scm.diff_for_file(baseline_fullpath, log=_log)
564            if output:
565                diff_file = get_result_file_fullpath(self._filesystem,
566                    self._options.html_directory, baseline_filename,
567                    self._platform, 'diff')
568                self._filesystem.write_text_file(diff_file, output)
569                _log.info('  Html: created baseline diff file: "%s".', diff_file)
570
571
572class HtmlGenerator(object):
573    """Class to generate rebaselining result comparison html."""
574
575    HTML_REBASELINE = ('<html>'
576                       '<head>'
577                       '<style>'
578                       'body {font-family: sans-serif;}'
579                       '.mainTable {background: #666666;}'
580                       '.mainTable td , .mainTable th {background: white;}'
581                       '.detail {margin-left: 10px; margin-top: 3px;}'
582                       '</style>'
583                       '<title>Rebaselining Result Comparison (%(time)s)'
584                       '</title>'
585                       '</head>'
586                       '<body>'
587                       '<h2>Rebaselining Result Comparison (%(time)s)</h2>'
588                       '%(body)s'
589                       '</body>'
590                       '</html>')
591    HTML_NO_REBASELINING_TESTS = (
592        '<p>No tests found that need rebaselining.</p>')
593    HTML_TABLE_TEST = ('<table class="mainTable" cellspacing=1 cellpadding=5>'
594                       '%s</table><br>')
595    HTML_TR_TEST = ('<tr>'
596                    '<th style="background-color: #CDECDE; border-bottom: '
597                    '1px solid black; font-size: 18pt; font-weight: bold" '
598                    'colspan="5">'
599                    '<a href="%s">%s</a>'
600                    '</th>'
601                    '</tr>')
602    HTML_TEST_DETAIL = ('<div class="detail">'
603                        '<tr>'
604                        '<th width="100">Baseline</th>'
605                        '<th width="100">Platform</th>'
606                        '<th width="200">Old</th>'
607                        '<th width="200">New</th>'
608                        '<th width="150">Difference</th>'
609                        '</tr>'
610                        '%s'
611                        '</div>')
612    HTML_TD_NOLINK = '<td align=center><a>%s</a></td>'
613    HTML_TD_LINK = '<td align=center><a href="%(uri)s">%(name)s</a></td>'
614    HTML_TD_LINK_IMG = ('<td><a href="%(uri)s">'
615                        '<img style="width: 200" src="%(uri)s" /></a></td>')
616    HTML_TR = '<tr>%s</tr>'
617
618    def __init__(self, port, target_port, options, platforms, rebaselining_tests):
619        self._html_directory = options.html_directory
620        self._port = port
621        self._target_port = target_port
622        self._options = options
623        self._platforms = platforms
624        self._rebaselining_tests = rebaselining_tests
625        self._filesystem = port._filesystem
626        self._html_file = self._filesystem.join(options.html_directory,
627                                                'rebaseline.html')
628
629    def abspath_to_uri(self, filename):
630        """Converts an absolute path to a file: URI."""
631        return path.abspath_to_uri(filename, self._port._executive)
632
633    def generate_html(self):
634        """Generate html file for rebaselining result comparison."""
635
636        _log.info('Generating html file')
637
638        html_body = ''
639        if not self._rebaselining_tests:
640            html_body += self.HTML_NO_REBASELINING_TESTS
641        else:
642            tests = list(self._rebaselining_tests)
643            tests.sort()
644
645            test_no = 1
646            for test in tests:
647                _log.info('Test %d: %s', test_no, test)
648                html_body += self._generate_html_for_one_test(test)
649
650        html = self.HTML_REBASELINE % ({'time': time.asctime(),
651                                        'body': html_body})
652        _log.debug(html)
653
654        self._filesystem.write_text_file(self._html_file, html)
655        _log.info('Baseline comparison html generated at "%s"', self._html_file)
656
657    def show_html(self):
658        """Launch the rebaselining html in brwoser."""
659
660        _log.info('Launching html: "%s"', self._html_file)
661        self._port._user.open_url(self._html_file)
662        _log.info('Html launched.')
663
664    def _generate_baseline_links(self, test_basename, suffix, platform):
665        """Generate links for baseline results (old, new and diff).
666
667        Args:
668          test_basename: base filename of the test
669          suffix: baseline file suffixes: '.txt', '.png'
670          platform: win, linux or mac
671
672        Returns:
673          html links for showing baseline results (old, new and diff)
674        """
675
676        baseline_filename = '%s-expected%s' % (test_basename, suffix)
677        _log.debug('    baseline filename: "%s"', baseline_filename)
678
679        new_file = get_result_file_fullpath(self._filesystem, self._html_directory,
680                                            baseline_filename, platform, 'new')
681        _log.info('    New baseline file: "%s"', new_file)
682        if not self._filesystem.exists(new_file):
683            _log.info('    No new baseline file: "%s"', new_file)
684            return ''
685
686        old_file = get_result_file_fullpath(self._filesystem, self._html_directory,
687                                            baseline_filename, platform, 'old')
688        _log.info('    Old baseline file: "%s"', old_file)
689        if suffix == '.png':
690            html_td_link = self.HTML_TD_LINK_IMG
691        else:
692            html_td_link = self.HTML_TD_LINK
693
694        links = ''
695        if self._filesystem.exists(old_file):
696            links += html_td_link % {
697                'uri': self.abspath_to_uri(old_file),
698                'name': baseline_filename}
699        else:
700            _log.info('    No old baseline file: "%s"', old_file)
701            links += self.HTML_TD_NOLINK % ''
702
703        links += html_td_link % {'uri': self.abspath_to_uri(new_file),
704                                 'name': baseline_filename}
705
706        diff_file = get_result_file_fullpath(self._filesystem, self._html_directory,
707                                             baseline_filename, platform, 'diff')
708        _log.info('    Baseline diff file: "%s"', diff_file)
709        if self._filesystem.exists(diff_file):
710            links += html_td_link % {'uri': self.abspath_to_uri(diff_file),
711                                     'name': 'Diff'}
712        else:
713            _log.info('    No baseline diff file: "%s"', diff_file)
714            links += self.HTML_TD_NOLINK % ''
715
716        return links
717
718    def _generate_html_for_one_test(self, test):
719        """Generate html for one rebaselining test.
720
721        Args:
722          test: layout test name
723
724        Returns:
725          html that compares baseline results for the test.
726        """
727
728        test_basename = self._filesystem.basename(self._filesystem.splitext(test)[0])
729        _log.info('  basename: "%s"', test_basename)
730        rows = []
731        for suffix in BASELINE_SUFFIXES:
732            if suffix == '.checksum':
733                continue
734
735            _log.info('  Checking %s files', suffix)
736            for platform in self._platforms:
737                links = self._generate_baseline_links(test_basename, suffix, platform)
738                if links:
739                    row = self.HTML_TD_NOLINK % self._get_baseline_result_type(suffix)
740                    row += self.HTML_TD_NOLINK % platform
741                    row += links
742                    _log.debug('    html row: %s', row)
743
744                    rows.append(self.HTML_TR % row)
745
746        if rows:
747            test_path = self._filesystem.join(self._target_port.layout_tests_dir(), test)
748            html = self.HTML_TR_TEST % (self.abspath_to_uri(test_path), test)
749            html += self.HTML_TEST_DETAIL % ' '.join(rows)
750
751            _log.debug('    html for test: %s', html)
752            return self.HTML_TABLE_TEST % html
753
754        return ''
755
756    def _get_baseline_result_type(self, suffix):
757        """Name of the baseline result type."""
758
759        if suffix == '.png':
760            return 'Pixel'
761        elif suffix == '.txt':
762            return 'Render Tree'
763        else:
764            return 'Other'
765
766
767def get_host_port_object(options):
768    """Return a port object for the platform we're running on."""
769    # The only thing we really need on the host is a way to diff
770    # text files and image files, which means we need to check that some
771    # version of ImageDiff has been built. We will look for either Debug
772    # or Release versions of the default port on the platform.
773    options.configuration = "Release"
774    port_obj = port.get(None, options)
775    if not port_obj.check_image_diff(override_step=None, logging=False):
776        _log.debug('No release version of the image diff binary was found.')
777        options.configuration = "Debug"
778        port_obj = port.get(None, options)
779        if not port_obj.check_image_diff(override_step=None, logging=False):
780            _log.error('No version of image diff was found. Check your build.')
781            return None
782        else:
783            _log.debug('Found the debug version of the image diff binary.')
784    else:
785        _log.debug('Found the release version of the image diff binary.')
786    return port_obj
787
788
789def parse_options(args):
790    """Parse options and return a pair of host options and target options."""
791    option_parser = optparse.OptionParser()
792    option_parser.add_option('-v', '--verbose',
793                             action='store_true',
794                             default=False,
795                             help='include debug-level logging.')
796
797    option_parser.add_option('-q', '--quiet',
798                             action='store_true',
799                             help='Suppress result HTML viewing')
800
801    option_parser.add_option('-p', '--platforms',
802                             default=None,
803                             help=('Comma delimited list of platforms '
804                                   'that need rebaselining.'))
805
806    option_parser.add_option('-u', '--archive_url',
807                             default=('http://build.chromium.org/f/chromium/'
808                                      'layout_test_results'),
809                             help=('Url to find the layout test result archive'
810                                   ' file.'))
811    option_parser.add_option('-U', '--force_archive_url',
812                             help=('Url of result zip file. This option is for debugging '
813                                   'purposes'))
814
815    option_parser.add_option('-b', '--backup',
816                             action='store_true',
817                             default=False,
818                             help=('Whether or not to backup the original test'
819                                   ' expectations file after rebaseline.'))
820
821    option_parser.add_option('-d', '--html_directory',
822                             default='',
823                             help=('The directory that stores the results for '
824                                   'rebaselining comparison.'))
825
826    option_parser.add_option('', '--use_drt',
827                             action='store_true',
828                             default=False,
829                             help=('Use ImageDiff from DumpRenderTree instead '
830                                   'of image_diff for pixel tests.'))
831
832    option_parser.add_option('-w', '--webkit_canary',
833                             action='store_true',
834                             default=False,
835                             help=('DEPRECATED. This flag no longer has any effect.'
836                                   '  The canaries are always used.'))
837
838    option_parser.add_option('', '--target-platform',
839                             default='chromium',
840                             help=('The target platform to rebaseline '
841                                   '("mac", "chromium", "qt", etc.). Defaults '
842                                   'to "chromium".'))
843
844    options = option_parser.parse_args(args)[0]
845    if options.webkit_canary:
846        print "-w/--webkit-canary is no longer necessary, ignoring."
847
848    target_options = copy.copy(options)
849    if options.target_platform == 'chromium':
850        target_options.chromium = True
851    options.tolerance = 0
852
853    return (options, target_options)
854
855
856def main(args):
857    """Bootstrap function that sets up the object references we need and calls real_main()."""
858    options, target_options = parse_options(args)
859
860    # Set up our logging format.
861    log_level = logging.INFO
862    if options.verbose:
863        log_level = logging.DEBUG
864    logging.basicConfig(level=log_level,
865                        format=('%(asctime)s %(filename)s:%(lineno)-3d '
866                                '%(levelname)s %(message)s'),
867                        datefmt='%y%m%d %H:%M:%S')
868
869    target_port_obj = port.get(None, target_options)
870    host_port_obj = get_host_port_object(options)
871    if not host_port_obj or not target_port_obj:
872        return 1
873
874    url_fetcher = urlfetcher.UrlFetcher(host_port_obj._filesystem)
875    scm_obj = scm.default_scm()
876
877    # We use the default zip factory method.
878    zip_factory = None
879
880    return real_main(options, target_options, host_port_obj, target_port_obj, url_fetcher,
881                     zip_factory, scm_obj)
882
883
884def real_main(options, target_options, host_port_obj, target_port_obj, url_fetcher,
885              zip_factory, scm_obj):
886    """Main function to produce new baselines. The Rebaseliner object uses two
887    different Port objects - one to represent the machine the object is running
888    on, and one to represent the port whose expectations are being updated.
889    E.g., you can run the script on a mac and rebaseline the 'win' port.
890
891    Args:
892        options: command-line argument used for the host_port_obj (see below)
893        target_options: command_line argument used for the target_port_obj.
894            This object may have slightly different values than |options|.
895        host_port_obj: a Port object for the platform the script is running
896            on. This is used to produce image and text diffs, mostly, and
897            is usually acquired from get_host_port_obj().
898        target_port_obj: a Port obj representing the port getting rebaselined.
899            This is used to find the expectations file, the baseline paths,
900            etc.
901        url_fetcher: object used to download the build archives from the bots
902        zip_factory: factory function used to create zip file objects for
903            the archives.
904        scm_obj: object used to add new baselines to the source control system.
905    """
906    options.html_directory = setup_html_directory(host_port_obj._filesystem, options.html_directory)
907    all_platforms = target_port_obj.all_baseline_variants()
908    if options.platforms:
909        bail = False
910        for platform in options.platforms:
911            if not platform in all_platforms:
912                _log.error('Invalid platform: "%s"' % (platform))
913                bail = True
914        if bail:
915            return 1
916        rebaseline_platforms = options.platforms
917    else:
918        rebaseline_platforms = all_platforms
919
920    rebaselined_tests = set()
921    for platform in rebaseline_platforms:
922        rebaseliner = Rebaseliner(host_port_obj, target_port_obj,
923                                  platform, options, url_fetcher, zip_factory,
924                                  scm_obj)
925
926        _log.info('')
927        log_dashed_string('Rebaseline started', platform)
928        if rebaseliner.run():
929            log_dashed_string('Rebaseline done', platform)
930        else:
931            log_dashed_string('Rebaseline failed', platform, logging.ERROR)
932
933        rebaselined_tests |= set(rebaseliner.get_rebaselined_tests())
934
935    if rebaselined_tests:
936        rebaseliner.remove_rebaselining_expectations(rebaselined_tests,
937                                                     options.backup)
938
939    _log.info('')
940    log_dashed_string('Rebaselining result comparison started', None)
941    html_generator = HtmlGenerator(host_port_obj,
942                                   target_port_obj,
943                                   options,
944                                   rebaseline_platforms,
945                                   rebaselined_tests)
946    html_generator.generate_html()
947    if not options.quiet:
948        html_generator.show_html()
949    log_dashed_string('Rebaselining result comparison done', None)
950
951    return 0
952
953
954if '__main__' == __name__:
955    sys.exit(main(sys.argv[1:]))
956