| # Copyright (c) 2010 Google Inc. All rights reserved. | |
| # | |
| # Redistribution and use in source and binary forms, with or without | |
| # modification, are permitted provided that the following conditions are | |
| # met: | |
| # | |
| # * Redistributions of source code must retain the above copyright | |
| # notice, this list of conditions and the following disclaimer. | |
| # * Redistributions in binary form must reproduce the above | |
| # copyright notice, this list of conditions and the following disclaimer | |
| # in the documentation and/or other materials provided with the | |
| # distribution. | |
| # * Neither the name of Google Inc. nor the names of its | |
| # contributors may be used to endorse or promote products derived from | |
| # this software without specific prior written permission. | |
| # | |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| import fnmatch | |
| import os | |
| import os.path | |
| import BaseHTTPServer | |
| from webkitpy.layout_tests.port.base import Port | |
| from webkitpy.tool.servers.reflection_handler import ReflectionHandler | |
| STATE_NEEDS_REBASELINE = 'needs_rebaseline' | |
| STATE_REBASELINE_FAILED = 'rebaseline_failed' | |
| STATE_REBASELINE_SUCCEEDED = 'rebaseline_succeeded' | |
| def _get_actual_result_files(test_file, test_config): | |
| test_name, _ = os.path.splitext(test_file) | |
| test_directory = os.path.dirname(test_file) | |
| test_results_directory = test_config.filesystem.join( | |
| test_config.results_directory, test_directory) | |
| actual_pattern = os.path.basename(test_name) + '-actual.*' | |
| actual_files = [] | |
| for filename in test_config.filesystem.listdir(test_results_directory): | |
| if fnmatch.fnmatch(filename, actual_pattern): | |
| actual_files.append(filename) | |
| actual_files.sort() | |
| return tuple(actual_files) | |
| def _rebaseline_test(test_file, baseline_target, baseline_move_to, test_config, log): | |
| test_name, _ = os.path.splitext(test_file) | |
| test_directory = os.path.dirname(test_name) | |
| log('Rebaselining %s...' % test_name) | |
| actual_result_files = _get_actual_result_files(test_file, test_config) | |
| filesystem = test_config.filesystem | |
| git = test_config.git | |
| layout_tests_directory = test_config.layout_tests_directory | |
| target_expectations_directory = filesystem.join( | |
| layout_tests_directory, 'platform', baseline_target, test_directory) | |
| test_results_directory = test_config.filesystem.join( | |
| test_config.results_directory, test_directory) | |
| # If requested, move current baselines out | |
| current_baselines = get_test_baselines(test_file, test_config) | |
| if baseline_target in current_baselines and baseline_move_to != 'none': | |
| log(' Moving current %s baselines to %s' % | |
| (baseline_target, baseline_move_to)) | |
| # See which ones we need to move (only those that are about to be | |
| # updated), and make sure we're not clobbering any files in the | |
| # destination. | |
| current_extensions = set(current_baselines[baseline_target].keys()) | |
| actual_result_extensions = [ | |
| os.path.splitext(f)[1] for f in actual_result_files] | |
| extensions_to_move = current_extensions.intersection( | |
| actual_result_extensions) | |
| if extensions_to_move.intersection( | |
| current_baselines.get(baseline_move_to, {}).keys()): | |
| log(' Already had baselines in %s, could not move existing ' | |
| '%s ones' % (baseline_move_to, baseline_target)) | |
| return False | |
| # Do the actual move. | |
| if extensions_to_move: | |
| if not _move_test_baselines( | |
| test_file, | |
| list(extensions_to_move), | |
| baseline_target, | |
| baseline_move_to, | |
| test_config, | |
| log): | |
| return False | |
| else: | |
| log(' No current baselines to move') | |
| log(' Updating baselines for %s' % baseline_target) | |
| filesystem.maybe_make_directory(target_expectations_directory) | |
| for source_file in actual_result_files: | |
| source_path = filesystem.join(test_results_directory, source_file) | |
| destination_file = source_file.replace('-actual', '-expected') | |
| destination_path = filesystem.join( | |
| target_expectations_directory, destination_file) | |
| filesystem.copyfile(source_path, destination_path) | |
| exit_code = git.add(destination_path, return_exit_code=True) | |
| if exit_code: | |
| log(' Could not update %s in SCM, exit code %d' % | |
| (destination_file, exit_code)) | |
| return False | |
| else: | |
| log(' Updated %s' % destination_file) | |
| return True | |
| def _move_test_baselines(test_file, extensions_to_move, source_platform, destination_platform, test_config, log): | |
| test_file_name = os.path.splitext(os.path.basename(test_file))[0] | |
| test_directory = os.path.dirname(test_file) | |
| filesystem = test_config.filesystem | |
| # Want predictable output order for unit tests. | |
| extensions_to_move.sort() | |
| source_directory = os.path.join( | |
| test_config.layout_tests_directory, | |
| 'platform', | |
| source_platform, | |
| test_directory) | |
| destination_directory = os.path.join( | |
| test_config.layout_tests_directory, | |
| 'platform', | |
| destination_platform, | |
| test_directory) | |
| filesystem.maybe_make_directory(destination_directory) | |
| for extension in extensions_to_move: | |
| file_name = test_file_name + '-expected' + extension | |
| source_path = filesystem.join(source_directory, file_name) | |
| destination_path = filesystem.join(destination_directory, file_name) | |
| filesystem.copyfile(source_path, destination_path) | |
| exit_code = test_config.git.add(destination_path, return_exit_code=True) | |
| if exit_code: | |
| log(' Could not update %s in SCM, exit code %d' % | |
| (file_name, exit_code)) | |
| return False | |
| else: | |
| log(' Moved %s' % file_name) | |
| return True | |
| def get_test_baselines(test_file, test_config): | |
| # FIXME: This seems like a hack. This only seems used to access the Port.expected_baselines logic. | |
| # Also, abstract method path_to_apache is not overidden; this will be fixed if this function is changed | |
| # to not use a sub-class of Port. pylint: disable=abstract-method | |
| class AllPlatformsPort(Port): | |
| def __init__(self, host): | |
| super(AllPlatformsPort, self).__init__(host, 'mac') | |
| self._platforms_by_directory = dict([(self._absolute_baseline_path(p), p) for p in test_config.platforms]) | |
| def baseline_search_path(self): | |
| return self._platforms_by_directory.keys() | |
| def platform_from_directory(self, directory): | |
| return self._platforms_by_directory[directory] | |
| all_platforms_port = AllPlatformsPort(test_config.host) | |
| all_test_baselines = {} | |
| for baseline_extension in ('.txt', '.checksum', '.png'): | |
| test_baselines = test_config.test_port.expected_baselines(test_file, baseline_extension) | |
| baselines = all_platforms_port.expected_baselines(test_file, baseline_extension, all_baselines=True) | |
| for platform_directory, expected_filename in baselines: | |
| if not platform_directory: | |
| continue | |
| if platform_directory == test_config.layout_tests_directory: | |
| platform = 'base' | |
| else: | |
| platform = all_platforms_port.platform_from_directory(platform_directory) | |
| platform_baselines = all_test_baselines.setdefault(platform, {}) | |
| was_used_for_test = (platform_directory, expected_filename) in test_baselines | |
| platform_baselines[baseline_extension] = was_used_for_test | |
| return all_test_baselines | |
| class RebaselineHTTPServer(BaseHTTPServer.HTTPServer): | |
| def __init__(self, httpd_port, config): | |
| server_name = '' | |
| BaseHTTPServer.HTTPServer.__init__(self, (server_name, httpd_port), RebaselineHTTPRequestHandler) | |
| self.test_config = config['test_config'] | |
| self.results_json = config['results_json'] | |
| self.platforms_json = config['platforms_json'] | |
| class RebaselineHTTPRequestHandler(ReflectionHandler): | |
| STATIC_FILE_NAMES = frozenset([ | |
| 'index.html', | |
| 'loupe.js', | |
| 'main.js', | |
| 'main.css', | |
| 'queue.js', | |
| 'util.js', | |
| ]) | |
| STATIC_FILE_DIRECTORY = os.path.join(os.path.dirname(__file__), 'data', 'rebaseline_server') | |
| def results_json(self): | |
| self._serve_json(self.server.results_json) | |
| def test_config(self): | |
| self._serve_json(self.server.test_config) | |
| def platforms_json(self): | |
| self._serve_json(self.server.platforms_json) | |
| def rebaseline(self): | |
| test = self.query['test'][0] | |
| baseline_target = self.query['baseline-target'][0] | |
| baseline_move_to = self.query['baseline-move-to'][0] | |
| test_json = self.server.results_json['tests'][test] | |
| if test_json['state'] != STATE_NEEDS_REBASELINE: | |
| self.send_error(400, 'Test %s is in unexpected state: %s' % (test, test_json['state'])) | |
| return | |
| log = [] | |
| success = _rebaseline_test( | |
| test, | |
| baseline_target, | |
| baseline_move_to, | |
| self.server.test_config, | |
| log=log.append) | |
| if success: | |
| test_json['state'] = STATE_REBASELINE_SUCCEEDED | |
| self.send_response(200) | |
| else: | |
| test_json['state'] = STATE_REBASELINE_FAILED | |
| self.send_response(500) | |
| self.send_header('Content-type', 'text/plain') | |
| self.end_headers() | |
| self.wfile.write('\n'.join(log)) | |
| def test_result(self): | |
| test_name, _ = os.path.splitext(self.query['test'][0]) | |
| mode = self.query['mode'][0] | |
| if mode == 'expected-image': | |
| file_name = test_name + '-expected.png' | |
| elif mode == 'actual-image': | |
| file_name = test_name + '-actual.png' | |
| if mode == 'expected-checksum': | |
| file_name = test_name + '-expected.checksum' | |
| elif mode == 'actual-checksum': | |
| file_name = test_name + '-actual.checksum' | |
| elif mode == 'diff-image': | |
| file_name = test_name + '-diff.png' | |
| if mode == 'expected-text': | |
| file_name = test_name + '-expected.txt' | |
| elif mode == 'actual-text': | |
| file_name = test_name + '-actual.txt' | |
| elif mode == 'diff-text': | |
| file_name = test_name + '-diff.txt' | |
| elif mode == 'diff-text-pretty': | |
| file_name = test_name + '-pretty-diff.html' | |
| file_path = os.path.join(self.server.test_config.results_directory, file_name) | |
| # Let results be cached for 60 seconds, so that they can be pre-fetched | |
| # by the UI | |
| self._serve_file(file_path, cacheable_seconds=60) |