PageRenderTime 65ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 0ms

/hooks/webkitpy/to_be_moved/rebaseline_chromium_webkit_tests.py

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