2 # Copyright (C) 2010 Google Inc. All rights reserved.
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
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
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.
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.
30 """Rebaselining tool that automatically produces baselines for all platforms.
32 The 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.
41 At the end, the script generates a html that compares old and new baselines.
44 from __future__ import with_statement
53 from webkitpy.common.checkout import scm
54 from webkitpy.common.system import zipfileset
55 from webkitpy.common.system import path
56 from webkitpy.common.system import urlfetcher
57 from webkitpy.common.system.executive import ScriptError
58 from webkitpy.common.host import Host
60 from webkitpy.layout_tests.port.factory import PortFactory
61 from webkitpy.layout_tests import read_checksum_from_png
62 from webkitpy.layout_tests.models import test_expectations
65 _log = logging.getLogger(__name__)
67 BASELINE_SUFFIXES = ('.txt', '.png', '.checksum')
69 ARCHIVE_DIR_NAME_DICT = {
70 'chromium-win-win7': 'Webkit_Win7',
71 'chromium-win-vista': 'Webkit_Vista',
72 'chromium-win-xp': 'Webkit_Win',
73 'chromium-mac-leopard': 'Webkit_Mac10_5',
74 'chromium-mac-snowleopard': 'Webkit_Mac10_6',
75 'chromium-cg-mac-leopard': 'Webkit_Mac10_5__CG_',
76 'chromium-cg-mac-snowleopard': 'Webkit_Mac10_6__CG_',
77 'chromium-linux-x86': 'Webkit_Linux_32',
78 'chromium-linux-x86_64': 'Webkit_Linux',
79 'chromium-gpu-mac-snowleopard': 'Webkit_Mac10_6_-_GPU',
80 'chromium-gpu-win-xp': 'Webkit_Win_-_GPU',
81 'chromium-gpu-win-win7': 'Webkit_Win7_-_GPU',
82 'chromium-gpu-linux-x86_64': 'Webkit_Linux_-_GPU',
83 'chromium-gpu-linux-x86': 'Webkit_Linux_32_-_GPU',
87 def log_dashed_string(text, platform=None, logging_level=logging.DEBUG):
88 """Log text message with dashes on both sides."""
91 msg += ': ' + platform
93 dashes = '-' * ((78 - len(msg)) / 2)
94 msg = '%s %s %s' % (dashes, msg, dashes)
95 _log.log(logging_level, msg)
98 def setup_html_directory(filesystem, parent_directory):
99 """Setup the directory to store html results.
101 All html related files are stored in the "rebaseline_html" subdirectory of
102 the parent directory. The path to the created directory is returned.
105 if not parent_directory:
106 parent_directory = str(filesystem.mkdtemp())
108 filesystem.maybe_make_directory(parent_directory)
110 html_directory = filesystem.join(parent_directory, 'rebaseline_html')
111 _log.debug('Html directory: "%s"', html_directory)
113 if filesystem.exists(html_directory):
114 filesystem.rmtree(html_directory)
115 _log.debug('Deleted html directory: "%s"', html_directory)
117 filesystem.maybe_make_directory(html_directory)
118 return html_directory
121 def get_result_file_fullpath(filesystem, html_directory, baseline_filename, platform,
123 """Get full path of the baseline result file.
126 filesystem: wrapper object
127 html_directory: directory that stores the html related files.
128 baseline_filename: name of the baseline file.
129 platform: win, linux or mac
130 result_type: type of the baseline result: '.txt', '.png'.
133 Full path of the baseline file for rebaselining result comparison.
136 base, ext = filesystem.splitext(baseline_filename)
137 result_filename = '%s-%s-%s%s' % (base, platform, result_type, ext)
138 fullpath = filesystem.join(html_directory, result_filename)
139 _log.debug(' Result file full path: "%s".', fullpath)
143 class Rebaseliner(object):
144 """Class to produce new baselines for a given platform."""
146 REVISION_REGEX = r'<a href=\"(\d+)/\">'
148 def __init__(self, running_port, target_port, platform, options, url_fetcher, zip_factory, scm, logged_before=False):
151 running_port: the Port the script is running on.
152 target_port: the Port the script uses to find port-specific
153 configuration information like the test_expectations.txt
154 file location and the list of test platforms.
155 platform: the test platform to rebaseline
156 options: the command-line options object.
157 url_fetcher: object that can fetch objects from URLs
158 zip_factory: optional object that can fetch zip files from URLs
159 scm: scm object for adding new baselines
160 logged_before: whether the previous running port logged anything.
162 self._platform = platform
163 self._options = options
164 self._port = running_port
165 self._filesystem = running_port._filesystem
166 self._target_port = target_port
168 # FIXME: This should get its PortFactory from a Host object.
169 # Note: using running_port.executive, running_port.user since we can't get them from a host.
170 self._rebaseline_port = PortFactory().get(platform, options, filesystem=self._filesystem, executive=running_port.executive, user=running_port.user)
171 self._rebaselining_tests = set()
172 self._rebaselined_tests = []
173 self._logged_before = logged_before
176 # Create tests and expectations helper which is used to:
177 # -. compile list of tests that need rebaselining.
178 # -. update the tests in test_expectations file after rebaseline
180 expectations_str = self._rebaseline_port.test_expectations()
181 self._test_expectations = test_expectations.TestExpectations(
182 self._rebaseline_port, None, expectations_str, self._rebaseline_port.test_configuration(), False)
183 self._url_fetcher = url_fetcher
184 self._zip_factory = zip_factory
188 """Run rebaseline process."""
190 log_dashed_string('Compiling rebaselining tests', self._platform, logging.DEBUG)
191 if not self._compile_rebaselining_tests():
193 if not self._rebaselining_tests:
197 log_dashed_string('Downloading archive', self._platform, logging.DEBUG)
198 archive_file = self._download_buildbot_archive()
201 _log.error('No archive found.')
204 log_dashed_string('Extracting and adding new baselines', self._platform, logging.DEBUG)
205 self._extract_and_add_new_baselines(archive_file)
208 log_dashed_string('Updating rebaselined tests in file', self._platform)
210 if len(self._rebaselining_tests) != len(self._rebaselined_tests):
212 _log.debug('NOT ALL TESTS WERE REBASELINED.')
213 _log.debug(' Number marked for rebaselining: %d', len(self._rebaselining_tests))
214 _log.debug(' Number actually rebaselined: %d', len(self._rebaselined_tests))
218 _log.debug(' All tests needing rebaselining were successfully rebaselined.')
222 def remove_rebaselining_expectations(self, tests, backup):
223 """if backup is True, we backup the original test expectations file."""
224 new_expectations = self._test_expectations.remove_rebaselined_tests(tests)
225 path = self._target_port.path_to_test_expectations_file()
227 date_suffix = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
228 backup_file = '%s.orig.%s' % (path, date_suffix)
229 if self._filesystem.exists(backup_file):
230 self._filesystem.remove(backup_file)
231 _log.debug('Saving original file to "%s"', backup_file)
232 self._filesystem.move(path, backup_file)
234 self._filesystem.write_text_file(path, new_expectations)
235 # self._scm.add(path)
237 def get_rebaselined_tests(self):
238 return self._rebaselined_tests
240 def _compile_rebaselining_tests(self):
241 """Compile list of tests that need rebaselining for the platform.
244 False if reftests are wrongly marked as 'needs rebaselining' or True
247 self._rebaselining_tests = self._test_expectations.get_rebaselining_failures()
248 if not self._rebaselining_tests:
249 _log.info('%s: No tests to rebaseline.', self._platform)
252 fs = self._target_port._filesystem
253 for test in self._rebaselining_tests:
254 test_abspath = self._target_port.abspath_for_test(test)
255 if (fs.exists(self._target_port.reftest_expected_filename(test_abspath)) or
256 fs.exists(self._target_port.reftest_expected_mismatch_filename(test_abspath))):
257 _log.error('%s seems to be a reftest. We can not rebase for reftests.', test)
258 self._rebaselining_tests = set()
261 if not self._logged_before:
263 _log.info('%s: Rebaselining %d tests:', self._platform, len(self._rebaselining_tests))
265 for test in self._rebaselining_tests:
266 _log.debug(' %d: %s', test_no, test)
271 def _get_latest_revision(self, url):
272 """Get the latest layout test revision number from buildbot.
275 url: Url to retrieve layout test revision numbers.
282 _log.debug('Url to retrieve revision: "%s"', url)
284 content = self._url_fetcher.fetch(url)
286 revisions = re.findall(self.REVISION_REGEX, content)
288 _log.error('Failed to find revision, content: "%s"', content)
291 revisions.sort(key=int)
292 _log.debug(' Latest revision: %s', revisions[len(revisions) - 1])
293 return revisions[len(revisions) - 1]
295 def _get_archive_dir_name(self, platform):
296 """Get name of the layout test archive directory.
303 if platform in ARCHIVE_DIR_NAME_DICT:
304 return ARCHIVE_DIR_NAME_DICT[platform]
306 _log.error('Cannot find platform key %s in archive '
307 'directory name dictionary', platform)
310 def _get_archive_url(self):
311 """Generate the url to download latest layout test archive.
314 Url to download archive or
318 if self._options.force_archive_url:
319 return self._options.force_archive_url
321 dir_name = self._get_archive_dir_name(self._platform)
325 _log.debug('Buildbot platform dir name: "%s"', dir_name)
327 url_base = '%s/%s/' % (self._options.archive_url, dir_name)
328 latest_revision = self._get_latest_revision(url_base)
329 if latest_revision is None or latest_revision <= 0:
331 archive_url = '%s%s/layout-test-results.zip' % (url_base, latest_revision)
332 _log.info(' Using %s', archive_url)
335 def _download_buildbot_archive(self):
336 """Download layout test archive file from buildbot and return a handle to it."""
337 url = self._get_archive_url()
341 archive_file = zipfileset.ZipFileSet(url, filesystem=self._filesystem,
342 zip_factory=self._zip_factory)
343 _log.debug('Archive downloaded')
346 def _extract_and_add_new_baselines(self, zip_file):
347 """Extract new baselines from the zip file and add them to SVN repository.
350 List of tests that have been rebaselined or None on failure."""
351 zip_namelist = zip_file.namelist()
353 _log.debug('zip file namelist:')
354 for name in zip_namelist:
355 _log.debug(' ' + name)
357 _log.debug('Platform dir: "%s"', self._platform)
359 self._rebaselined_tests = []
360 for test_no, test in enumerate(self._rebaselining_tests):
361 _log.debug('Test %d: %s', test_no + 1, test)
362 self._extract_and_add_new_baseline(test, zip_file)
364 def _extract_and_add_new_baseline(self, test, zip_file):
367 test_basename = self._filesystem.splitext(test)[0]
368 for suffix in BASELINE_SUFFIXES:
369 archive_test_name = 'layout-test-results/%s-actual%s' % (test_basename, suffix)
370 _log.debug(' Archive test file name: "%s"', archive_test_name)
371 if not archive_test_name in zip_file.namelist():
372 _log.debug(' %s file not in archive.', suffix)
376 _log.debug(' %s file found in archive.', suffix)
378 temp_name = self._extract_from_zip_to_tempfile(zip_file, archive_test_name)
380 expected_filename = '%s-expected%s' % (test_basename, suffix)
381 expected_fullpath = self._filesystem.join(
382 self._rebaseline_port.baseline_path(), expected_filename)
383 expected_fullpath = self._filesystem.normpath(expected_fullpath)
384 _log.debug(' Expected file full path: "%s"', expected_fullpath)
386 relpath = self._filesystem.relpath(expected_fullpath, self._target_port.layout_tests_dir())
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 if self._filesystem.exists(expected_fullpath):
395 _log.info(' Removing %s' % relpath)
396 self._delete_baseline(expected_fullpath)
397 _log.debug(' %s is a duplicate' % relpath)
399 # FIXME: We consider a duplicate baseline a success in the normal case.
400 # FIXME: This may not be what you want sometimes; should this be
401 # FIXME: controllable?
402 self._rebaselined_tests.append(test)
405 if suffix == '.checksum' and self._png_has_same_checksum(temp_name, test, expected_fullpath):
406 self._filesystem.remove(temp_name)
407 # If an old checksum exists, delete it.
408 self._delete_baseline(expected_fullpath)
411 self._filesystem.maybe_make_directory(self._filesystem.dirname(expected_fullpath))
412 self._filesystem.move(temp_name, expected_fullpath)
414 path_from_base = self._filesystem.relpath(expected_fullpath)
415 if self._scm.exists(path_from_base):
416 _log.info(' Updating %s' % relpath)
418 _log.info(' Adding %s' % relpath)
420 if self._scm.add(expected_fullpath, return_exit_code=True):
421 # FIXME: print detailed diagnose messages
423 elif suffix != '.checksum':
424 self._create_html_baseline_files(expected_fullpath)
427 _log.warn('No results in archive for %s' % test)
429 _log.warn('Failed to add baselines to your repository.')
431 _log.debug(' Rebaseline succeeded.')
432 self._rebaselined_tests.append(test)
434 def _extract_from_zip_to_tempfile(self, zip_file, filename):
435 """Extracts |filename| from |zip_file|, a ZipFileSet. Returns the full
436 path name to the extracted file."""
437 data = zip_file.read(filename)
438 suffix = self._filesystem.splitext(filename)[1]
439 tempfile, temp_name = self._filesystem.open_binary_tempfile(suffix)
444 def _png_has_same_checksum(self, checksum_path, test, checksum_expected_fullpath):
445 """Returns True if the fallback png for |checksum_expected_fullpath|
446 contains the same checksum."""
447 fs = self._filesystem
448 png_fullpath = self._first_fallback_png_for_test(test)
450 if not fs.exists(png_fullpath):
451 _log.error(' Checksum without png file found! Expected %s to exist.' % png_fullpath)
454 with fs.open_binary_file_for_reading(png_fullpath) as filehandle:
455 checksum_in_png = read_checksum_from_png.read_checksum(filehandle)
456 checksum_in_text_file = fs.read_text_file(checksum_path)
457 if checksum_in_png and checksum_in_png != checksum_in_text_file:
458 _log.error(" checksum in %s and %s don't match! Continuing"
459 " to copy but please investigate." % (
460 checksum_expected_fullpath, png_fullpath))
461 return checksum_in_text_file == checksum_in_png
463 def _first_fallback_png_for_test(self, test):
464 all_baselines = self._rebaseline_port.expected_baselines(test, '.png', True)
465 return self._filesystem.join(all_baselines[0][0], all_baselines[0][1])
467 def _is_dup_baseline(self, new_baseline, baseline_path, test, suffix, platform):
468 """Check whether a baseline is duplicate and can fallback to same
469 baseline for another platform. For example, if a test has same
470 baseline on linux and windows, then we only store windows
471 baseline and linux baseline will fallback to the windows version.
474 new_baseline: temp filename containing the new baseline results
475 baseline_path: baseline expectation file name.
477 suffix: file suffix of the expected results, including dot;
478 e.g. '.txt' or '.png'.
479 platform: baseline platform 'mac', 'win' or 'linux'.
482 True if the baseline is unnecessary.
485 all_baselines = self._rebaseline_port.expected_baselines(test, suffix, True)
487 for fallback_dir, fallback_file in all_baselines:
488 if not fallback_dir or not fallback_file:
491 fallback_fullpath = self._filesystem.normpath(
492 self._filesystem.join(fallback_dir, fallback_file))
493 if fallback_fullpath.lower() == baseline_path.lower():
495 fallback_dir_relpath = self._filesystem.relpath(fallback_dir, self._target_port.layout_tests_dir())
496 if fallback_dir_relpath == '':
497 fallback_dir_relpath = '<generic>'
499 new_output = self._filesystem.read_binary_file(new_baseline)
500 fallback_output = self._filesystem.read_binary_file(fallback_fullpath)
501 is_image = baseline_path.lower().endswith('.png')
502 if not self._diff_baselines(new_output, fallback_output, is_image):
503 _log.info(' Skipping %s (matches %s)', test, fallback_dir_relpath)
509 def _diff_baselines(self, output1, output2, is_image):
510 """Check whether two baselines are different.
513 output1, output2: contents of the baselines to compare.
516 True if two files are different or have different extensions.
521 return self._port.diff_image(output1, output2)[0]
523 return self._port.compare_text(output1, output2)
525 def _delete_baseline(self, filename):
526 """Remove the file from repository and delete it from disk.
529 filename: full path of the file to delete.
532 if not filename or not self._filesystem.isfile(filename):
534 self._scm.delete(filename)
536 def _create_html_baseline_files(self, baseline_fullpath):
537 """Create baseline files (old, new and diff) in html directory.
539 The files are used to compare the rebaselining results.
542 baseline_fullpath: full path of the expected baseline file.
545 baseline_relpath = self._filesystem.relpath(baseline_fullpath)
546 _log.debug(' Html: create baselines for "%s"', baseline_relpath)
548 if (not baseline_fullpath
549 or not self._filesystem.exists(baseline_fullpath)):
550 _log.debug(' Html: Does not exist: "%s"', baseline_fullpath)
553 if not self._scm.exists(baseline_relpath):
554 _log.debug(' Html: Does not exist in scm: "%s"', baseline_relpath)
557 # Copy the new baseline to html directory for result comparison.
558 baseline_filename = self._filesystem.basename(baseline_fullpath)
559 new_file = get_result_file_fullpath(self._filesystem, self._options.html_directory,
560 baseline_filename, self._platform, 'new')
561 self._filesystem.copyfile(baseline_fullpath, new_file)
562 _log.debug(' Html: copied new baseline file from "%s" to "%s".',
563 baseline_fullpath, new_file)
565 # Get the old baseline from the repository and save to the html directory.
567 output = self._scm.show_head(baseline_relpath)
568 except ScriptError, e:
572 if (not output) or (output.upper().rstrip().endswith('NO SUCH FILE OR DIRECTORY')):
573 _log.warning(' No base file: "%s"', baseline_fullpath)
575 base_file = get_result_file_fullpath(self._filesystem, self._options.html_directory,
576 baseline_filename, self._platform, 'old')
577 if base_file.upper().endswith('.PNG'):
578 self._filesystem.write_binary_file(base_file, output)
580 self._filesystem.write_text_file(base_file, output)
581 _log.debug(' Html: created old baseline file: "%s".', base_file)
583 # Get the diff between old and new baselines and save to the html dir.
584 diff_file = get_result_file_fullpath(self._filesystem,
585 self._options.html_directory,
587 self._platform, 'diff')
589 if baseline_filename.upper().endswith('.TXT'):
590 output = self._scm.diff_for_file(baseline_relpath, log=_log)
592 self._filesystem.write_text_file(diff_file, output)
594 elif baseline_filename.upper().endswith('.PNG'):
595 old_file = get_result_file_fullpath(self._filesystem,
596 self._options.html_directory,
598 self._platform, 'old')
599 new_file = get_result_file_fullpath(self._filesystem,
600 self._options.html_directory,
602 self._platform, 'new')
603 _log.debug(' Html: diffing "%s" and "%s"', old_file, new_file)
604 old_output = self._filesystem.read_binary_file(old_file)
605 new_output = self._filesystem.read_binary_file(new_file)
606 image_diff = self._port.diff_image(old_output, new_output)[0]
607 self._filesystem.write_binary_file(diff_file, image_diff)
610 _log.debug(' Html: created baseline diff file: "%s".', diff_file)
613 class HtmlGenerator(object):
614 """Class to generate rebaselining result comparison html."""
616 HTML_REBASELINE = ('<html>'
619 'body {font-family: sans-serif;}'
620 '.mainTable {background: #666666;}'
621 '.mainTable td , .mainTable th {background: white;}'
622 '.detail {margin-left: 10px; margin-top: 3px;}'
624 '<title>Rebaselining Result Comparison (%(time)s)'
628 '<h2>Rebaselining Result Comparison (%(time)s)</h2>'
632 HTML_NO_REBASELINING_TESTS = (
633 '<p>No tests found that need rebaselining.</p>')
634 HTML_TABLE_TEST = ('<table class="mainTable" cellspacing=1 cellpadding=5>'
636 HTML_TR_TEST = ('<tr>'
637 '<th style="background-color: #CDECDE; border-bottom: '
638 '1px solid black; font-size: 18pt; font-weight: bold" '
640 '<a href="%s">%s</a>'
643 HTML_TEST_DETAIL = ('<div class="detail">'
645 '<th width="100">Baseline</th>'
646 '<th width="100">Platform</th>'
647 '<th width="200">Old</th>'
648 '<th width="200">New</th>'
649 '<th width="150">Difference</th>'
653 HTML_TD_NOLINK = '<td align=center><a>%s</a></td>'
654 HTML_TD_LINK = '<td align=center><a href="%(uri)s">%(name)s</a></td>'
655 HTML_TD_LINK_IMG = ('<td><a href="%(uri)s">'
656 '<img style="width: 200" src="%(uri)s" /></a></td>')
657 HTML_TR = '<tr>%s</tr>'
659 def __init__(self, port, target_port, options, platforms, rebaselining_tests):
660 self._html_directory = options.html_directory
662 self._target_port = target_port
663 self._options = options
664 self._platforms = platforms
665 self._rebaselining_tests = rebaselining_tests
666 self._filesystem = port._filesystem
667 self._html_file = self._filesystem.join(options.html_directory,
670 def abspath_to_uri(self, filename):
671 """Converts an absolute path to a file: URI."""
672 return path.abspath_to_uri(filename, self._port._executive)
674 def generate_html(self):
675 """Generate html file for rebaselining result comparison."""
677 _log.debug('Generating html file')
680 if not self._rebaselining_tests:
681 html_body += self.HTML_NO_REBASELINING_TESTS
683 tests = list(self._rebaselining_tests)
688 _log.debug('Test %d: %s', test_no, test)
689 html_body += self._generate_html_for_one_test(test)
691 html = self.HTML_REBASELINE % ({'time': time.asctime(),
695 self._filesystem.write_text_file(self._html_file, html)
696 _log.debug('Baseline comparison html generated at "%s"', self._html_file)
699 """Launch the rebaselining html in brwoser."""
701 _log.debug('Launching html: "%s"', self._html_file)
702 self._port._user.open_url(self._html_file)
703 _log.debug('Html launched.')
705 def _generate_baseline_links(self, test_basename, suffix, platform):
706 """Generate links for baseline results (old, new and diff).
709 test_basename: base filename of the test
710 suffix: baseline file suffixes: '.txt', '.png'
711 platform: win, linux or mac
714 html links for showing baseline results (old, new and diff)
717 baseline_filename = '%s-expected%s' % (test_basename, suffix)
718 _log.debug(' baseline filename: "%s"', baseline_filename)
720 new_file = get_result_file_fullpath(self._filesystem, self._html_directory,
721 baseline_filename, platform, 'new')
722 _log.debug(' New baseline file: "%s"', new_file)
723 if not self._filesystem.exists(new_file):
724 _log.debug(' No new baseline file: "%s"', new_file)
727 old_file = get_result_file_fullpath(self._filesystem, self._html_directory,
728 baseline_filename, platform, 'old')
729 _log.debug(' Old baseline file: "%s"', old_file)
731 html_td_link = self.HTML_TD_LINK_IMG
733 html_td_link = self.HTML_TD_LINK
736 if self._filesystem.exists(old_file):
737 links += html_td_link % {
738 'uri': self.abspath_to_uri(old_file),
739 'name': baseline_filename}
741 _log.debug(' No old baseline file: "%s"', old_file)
742 links += self.HTML_TD_NOLINK % ''
744 links += html_td_link % {'uri': self.abspath_to_uri(new_file),
745 'name': baseline_filename}
747 diff_file = get_result_file_fullpath(self._filesystem, self._html_directory,
748 baseline_filename, platform, 'diff')
749 _log.debug(' Baseline diff file: "%s"', diff_file)
750 if self._filesystem.exists(diff_file):
751 links += html_td_link % {'uri': self.abspath_to_uri(diff_file),
754 _log.debug(' No baseline diff file: "%s"', diff_file)
755 links += self.HTML_TD_NOLINK % ''
759 def _generate_html_for_one_test(self, test):
760 """Generate html for one rebaselining test.
763 test: layout test name
766 html that compares baseline results for the test.
769 test_basename = self._filesystem.basename(self._filesystem.splitext(test)[0])
770 _log.debug(' basename: "%s"', test_basename)
772 for suffix in BASELINE_SUFFIXES:
773 if suffix == '.checksum':
776 _log.debug(' Checking %s files', suffix)
777 for platform in self._platforms:
778 links = self._generate_baseline_links(test_basename, suffix, platform)
780 row = self.HTML_TD_NOLINK % self._get_baseline_result_type(suffix)
781 row += self.HTML_TD_NOLINK % platform
783 _log.debug(' html row: %s', row)
785 rows.append(self.HTML_TR % row)
788 test_path = self._filesystem.join(self._target_port.layout_tests_dir(), test)
789 html = self.HTML_TR_TEST % (self.abspath_to_uri(test_path), test)
790 html += self.HTML_TEST_DETAIL % ' '.join(rows)
792 _log.debug(' html for test: %s', html)
793 return self.HTML_TABLE_TEST % html
797 def _get_baseline_result_type(self, suffix):
798 """Name of the baseline result type."""
802 elif suffix == '.txt':
808 def get_host_port_object(port_factory, options):
809 """Return a port object for the platform we're running on."""
810 # We want the ImageDiff logic to match that of the chromium bots, so we
811 # force the use of a Chromium port. We will look for either Debug or
813 options.configuration = "Release"
814 options.chromium = True
815 port_obj = port_factory.get(options=options)
816 if not port_obj.check_image_diff(override_step=None, logging=False):
817 _log.debug('No release version of the image diff binary was found.')
818 options.configuration = "Debug"
819 port_obj = port_factory.get(options=options)
820 if not port_obj.check_image_diff(override_step=None, logging=False):
821 _log.error('No version of image diff was found. Check your build.')
824 _log.debug('Found the debug version of the image diff binary.')
826 _log.debug('Found the release version of the image diff binary.')
830 def parse_options(args):
831 """Parse options and return a pair of host options and target options."""
832 option_parser = optparse.OptionParser()
833 option_parser.add_option('-v', '--verbose',
836 help='include debug-level logging.')
838 option_parser.add_option('-q', '--quiet',
840 help='Suppress result HTML viewing')
842 option_parser.add_option('-p', '--platforms',
844 help=('Comma delimited list of platforms '
845 'that need rebaselining.'))
847 option_parser.add_option('-u', '--archive_url',
848 default=('http://build.chromium.org/f/chromium/'
849 'layout_test_results'),
850 help=('Url to find the layout test result archive'
852 option_parser.add_option('-U', '--force_archive_url',
853 help=('Url of result zip file. This option is for debugging '
856 option_parser.add_option('-b', '--backup',
859 help=('Whether or not to backup the original test'
860 ' expectations file after rebaseline.'))
862 option_parser.add_option('-d', '--html_directory',
864 help=('The directory that stores the results for '
865 'rebaselining comparison.'))
867 option_parser.add_option('', '--use_drt',
870 help=('Use ImageDiff from DumpRenderTree instead '
871 'of image_diff for pixel tests.'))
873 option_parser.add_option('-w', '--webkit_canary',
876 help=('DEPRECATED. This flag no longer has any effect.'
877 ' The canaries are always used.'))
879 option_parser.add_option('', '--target-platform',
881 help=('The target platform to rebaseline '
882 '("mac", "chromium", "qt", etc.). Defaults '
885 options = option_parser.parse_args(args)[0]
886 if options.webkit_canary:
887 print "-w/--webkit-canary is no longer necessary, ignoring."
889 target_options = copy.copy(options)
890 if options.target_platform == 'chromium':
891 target_options.chromium = True
892 options.tolerance = 0
894 return (options, target_options)
897 class DebugLogHandler(logging.Handler):
901 logging.Handler.__init__(self)
902 self.formatter = logging.Formatter(fmt=('%(asctime)s %(filename)s:%(lineno)-3d '
903 '%(levelname)s %(message)s'))
904 self.setFormatter(self.formatter)
906 def emit(self, record):
907 if record.levelno > logging.INFO:
908 self.num_failures += 1
909 print self.format(record)
912 class NormalLogHandler(logging.Handler):
916 def emit(self, record):
917 if record.levelno > logging.INFO:
918 self.num_failures += 1
919 if self.last_levelno != record.levelno:
921 self.last_levelno = record.levelno
923 msg = record.getMessage()
924 if record.levelno > logging.INFO and msg:
925 prefix = '%s: ' % record.levelname
926 print '%s%s' % (prefix, msg)
930 """Bootstrap function that sets up the object references we need and calls real_main()."""
931 options, target_options = parse_options(args)
933 logger = logging.getLogger()
934 logger.setLevel(logging.INFO)
936 log_level = logging.DEBUG
937 log_handler = DebugLogHandler()
939 log_level = logging.INFO
940 log_handler = NormalLogHandler()
942 logger = logging.getLogger()
943 logger.setLevel(log_level)
944 logger.addHandler(log_handler)
947 host._initialize_scm()
948 target_port_obj = host.port_factory.get(None, target_options)
949 host_port_obj = get_host_port_object(host.port_factory, options)
950 if not host_port_obj or not target_port_obj:
953 url_fetcher = urlfetcher.UrlFetcher(host_port_obj._filesystem)
955 # We use the default zip factory method.
958 # FIXME: SCM module doesn't handle paths that aren't relative to the checkout_root consistently.
959 host_port_obj._filesystem.chdir(host.scm().checkout_root)
961 ret_code = real_main(options, target_options, host_port_obj, target_port_obj, url_fetcher, zip_factory, host.scm())
962 if not ret_code and log_handler.num_failures:
966 print 'Rebaselining failed.'
968 print 'Rebaselining succeeded.'
972 def real_main(options, target_options, host_port_obj, target_port_obj, url_fetcher, zip_factory, scm_obj):
973 """Main function to produce new baselines. The Rebaseliner object uses two
974 different Port objects - one to represent the machine the object is running
975 on, and one to represent the port whose expectations are being updated.
976 E.g., you can run the script on a mac and rebaseline the 'win' port.
979 options: command-line argument used for the host_port_obj (see below)
980 target_options: command_line argument used for the target_port_obj.
981 This object may have slightly different values than |options|.
982 host_port_obj: a Port object for the platform the script is running
983 on. This is used to produce image and text diffs, mostly, and
984 is usually acquired from get_host_port_obj().
985 target_port_obj: a Port obj representing the port getting rebaselined.
986 This is used to find the expectations file, the baseline paths,
988 url_fetcher: object used to download the build archives from the bots
989 zip_factory: factory function used to create zip file objects for
991 scm_obj: object used to add new baselines to the source control system.
993 options.html_directory = setup_html_directory(host_port_obj._filesystem, options.html_directory)
994 all_platforms = target_port_obj.all_baseline_variants()
995 if options.platforms:
997 for platform in options.platforms:
998 if not platform in all_platforms:
999 _log.error('Invalid platform: "%s"' % (platform))
1003 rebaseline_platforms = options.platforms
1005 rebaseline_platforms = all_platforms
1007 # FIXME: These log messages will be wrong if ports store baselines outside
1008 # of layout_tests_dir(), but the code should work correctly.
1009 layout_tests_dir = target_port_obj.layout_tests_dir()
1010 expectations_path = target_port_obj.path_to_test_expectations_file()
1011 _log.info('Using %s' % layout_tests_dir)
1012 _log.info(' and %s' % expectations_path)
1014 rebaselined_tests = set()
1015 logged_before = False
1016 for platform in rebaseline_platforms:
1017 rebaseliner = Rebaseliner(host_port_obj, target_port_obj,
1018 platform, options, url_fetcher, zip_factory,
1019 scm_obj, logged_before)
1022 log_dashed_string('Rebaseline started', platform)
1023 if rebaseliner.run():
1024 log_dashed_string('Rebaseline done', platform)
1026 log_dashed_string('Rebaseline failed', platform)
1028 rebaselined_tests |= set(rebaseliner.get_rebaselined_tests())
1029 logged_before = rebaseliner.did_log
1031 if rebaselined_tests:
1032 rebaseliner.remove_rebaselining_expectations(rebaselined_tests,
1036 log_dashed_string('Rebaselining result comparison started')
1037 html_generator = HtmlGenerator(host_port_obj,
1040 rebaseline_platforms,
1042 html_generator.generate_html()
1043 if not options.quiet:
1044 html_generator.show_html()
1045 log_dashed_string('Rebaselining result comparison done')
1050 if '__main__' == __name__:
1051 sys.exit(main(sys.argv[1:]))