blob: 067d358940419b7c74715a3ef0a847617df260ef [file] [log] [blame]
# Copyright (C) 2011, 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 copy
import logging
import functools
from webkitpy.common.memoized import memoized
_log = logging.getLogger(__name__)
class BaselineOptimizer(object):
ROOT_LAYOUT_TESTS_DIRECTORY = 'LayoutTests'
def __init__(self, host, port, port_names):
self._filesystem = host.filesystem
self._default_port = port
self._ports = {}
for port_name in port_names:
self._ports[port_name] = host.port_factory.get(port_name)
self._webkit_base = port.webkit_base()
self._layout_tests_dir = port.layout_tests_dir()
# Only used by unit tests.
self.new_results_by_directory = []
def optimize(self, baseline_name):
# The virtual fallback path is the same as the non-virtual one
# tacked on to the bottom of the non-virtual path. See
# https://docs.google.com/a/chromium.org/drawings/d/1eGdsIKzJ2dxDDBbUaIABrN4aMLD1bqJTfyxNGZsTdmg/edit
# for a visual representation of this.
# So, we can optimize the virtual path, then the virtual root, and then
# the regular path.
_log.debug('Optimizing regular fallback path.')
result = self._optimize_subtree(baseline_name)
non_virtual_baseline_name = self._virtual_base(baseline_name)
if not non_virtual_baseline_name:
return result
self._optimize_virtual_root(baseline_name, non_virtual_baseline_name)
_log.debug('Optimizing non-virtual fallback path.')
result |= self._optimize_subtree(non_virtual_baseline_name)
return result
def write_by_directory(self, results_by_directory, writer, indent):
for path in sorted(results_by_directory):
writer('%s%s: %s' % (indent, self._platform(path), results_by_directory[path][0:6]))
def read_results_by_directory(self, baseline_name):
results_by_directory = {}
directories = functools.reduce(set.union, map(set, [self._relative_baseline_search_paths(
port, baseline_name) for port in self._ports.values()]))
for directory in directories:
path = self._join_directory(directory, baseline_name)
if self._filesystem.exists(path):
results_by_directory[directory] = self._filesystem.sha1(path)
return results_by_directory
def _optimize_subtree(self, baseline_name):
basename = self._filesystem.basename(baseline_name)
results_by_directory, new_results_by_directory = self._find_optimal_result_placement(baseline_name)
if new_results_by_directory == results_by_directory:
if new_results_by_directory:
_log.debug(' %s: (already optimal)', basename)
self.write_by_directory(results_by_directory, _log.debug, ' ')
else:
_log.debug(' %s: (no baselines found)', basename)
# This is just used for unit tests.
# Intentionally set it to the old data if we don't modify anything.
self.new_results_by_directory.append(results_by_directory)
return True
if (self._results_by_port_name(results_by_directory, baseline_name) !=
self._results_by_port_name(new_results_by_directory, baseline_name)):
# This really should never happen. Just a sanity check to make
# sure the script fails in the case of bugs instead of committing
# incorrect baselines.
_log.error(' %s: optimization failed', basename)
self.write_by_directory(results_by_directory, _log.warning, ' ')
return False
_log.debug(' %s:', basename)
_log.debug(' Before: ')
self.write_by_directory(results_by_directory, _log.debug, ' ')
_log.debug(' After: ')
self.write_by_directory(new_results_by_directory, _log.debug, ' ')
self._move_baselines(baseline_name, results_by_directory, new_results_by_directory)
return True
def _move_baselines(self, baseline_name, results_by_directory, new_results_by_directory):
data_for_result = {}
for directory, result in results_by_directory.items():
if result not in data_for_result:
source = self._join_directory(directory, baseline_name)
data_for_result[result] = self._filesystem.read_binary_file(source)
fs_files = []
for directory, result in results_by_directory.items():
if new_results_by_directory.get(directory) != result:
file_name = self._join_directory(directory, baseline_name)
if self._filesystem.exists(file_name):
fs_files.append(file_name)
if fs_files:
_log.debug(' Deleting (file system):')
for platform_dir in sorted(self._platform(filename) for filename in fs_files):
_log.debug(' ' + platform_dir)
for filename in fs_files:
self._filesystem.remove(filename)
else:
_log.debug(' (Nothing to delete)')
file_names = []
for directory, result in new_results_by_directory.items():
if results_by_directory.get(directory) != result:
destination = self._join_directory(directory, baseline_name)
self._filesystem.maybe_make_directory(self._filesystem.split(destination)[0])
self._filesystem.write_binary_file(destination, data_for_result[result])
file_names.append(destination)
if file_names:
_log.debug(' Adding:')
for platform_dir in sorted(self._platform(filename) for filename in file_names):
_log.debug(' ' + platform_dir)
else:
_log.debug(' (Nothing to add)')
def _platform(self, filename):
platform_dir = self.ROOT_LAYOUT_TESTS_DIRECTORY + self._filesystem.sep + 'platform' + self._filesystem.sep
if filename.startswith(platform_dir):
return filename.replace(platform_dir, '').split(self._filesystem.sep)[0]
platform_dir = self._filesystem.join(self._webkit_base, platform_dir)
if filename.startswith(platform_dir):
return filename.replace(platform_dir, '').split(self._filesystem.sep)[0]
return '(generic)'
def _optimize_virtual_root(self, baseline_name, non_virtual_baseline_name):
virtual_root_baseline_path = self._filesystem.join(self._layout_tests_dir, baseline_name)
if not self._filesystem.exists(virtual_root_baseline_path):
return
root_sha1 = self._filesystem.sha1(virtual_root_baseline_path)
results_by_directory = self.read_results_by_directory(non_virtual_baseline_name)
# See if all the immediate predecessors of the virtual root have the same expected result.
for port in self._ports.values():
directories = self._relative_baseline_search_paths(port, non_virtual_baseline_name)
for directory in directories:
if directory not in results_by_directory:
continue
if results_by_directory[directory] != root_sha1:
return
break
_log.debug('Deleting redundant virtual root expected result.')
_log.debug(' Deleting (file system): ' + virtual_root_baseline_path)
self._filesystem.remove(virtual_root_baseline_path)
def _baseline_root(self, baseline_name):
virtual_suite = self._virtual_suite(baseline_name)
if virtual_suite:
return self._filesystem.join(self.ROOT_LAYOUT_TESTS_DIRECTORY, virtual_suite.name)
return self.ROOT_LAYOUT_TESTS_DIRECTORY
def _baseline_search_path(self, port, baseline_name):
virtual_suite = self._virtual_suite(baseline_name)
if virtual_suite:
return port.virtual_baseline_search_path(baseline_name)
return port.baseline_search_path()
def _virtual_suite(self, baseline_name):
return self._default_port.lookup_virtual_suite(baseline_name)
def _virtual_base(self, baseline_name):
return self._default_port.lookup_virtual_test_base(baseline_name)
def _relative_baseline_search_paths(self, port, baseline_name):
"""Returns a list of paths to check for baselines in order."""
baseline_search_path = self._baseline_search_path(port, baseline_name)
baseline_root = self._baseline_root(baseline_name)
relative_paths = [self._filesystem.relpath(path, self._webkit_base) for path in baseline_search_path]
return relative_paths + [baseline_root]
def _join_directory(self, directory, baseline_name):
# This code is complicated because both the directory name and the
# baseline_name have the virtual test suite in the name and the virtual
# baseline name is not a strict superset of the non-virtual name.
# For example, virtual/gpu/fast/canvas/foo-expected.png corresponds to
# fast/canvas/foo-expected.png and the baseline directories are like
# platform/mac/virtual/gpu/fast/canvas. So, to get the path to the
# baseline in the platform directory, we need to append just
# foo-expected.png to the directory.
virtual_suite = self._virtual_suite(baseline_name)
if virtual_suite:
baseline_name_without_virtual = baseline_name[len(virtual_suite.name) + 1:]
else:
baseline_name_without_virtual = baseline_name
return self._filesystem.join(self._webkit_base, directory, baseline_name_without_virtual)
def _results_by_port_name(self, results_by_directory, baseline_name):
results_by_port_name = {}
for port_name, port in self._ports.items():
for directory in self._relative_baseline_search_paths(port, baseline_name):
if directory in results_by_directory:
results_by_port_name[port_name] = results_by_directory[directory]
break
return results_by_port_name
@memoized
def _directories_immediately_preceding_root(self, baseline_name):
directories = set()
for port in self._ports.values():
directory = self._filesystem.relpath(self._baseline_search_path(port, baseline_name)[-1], self._webkit_base)
directories.add(directory)
return directories
def _optimize_result_for_root(self, new_results_by_directory, baseline_name):
# The root directory (i.e. LayoutTests) is the only one that doesn't correspond
# to a specific platform. As such, it's the only one where the baseline in fallback directories
# immediately before it can be promoted up, i.e. if win and mac
# have the same baseline, then it can be promoted up to be the LayoutTests baseline.
# All other baselines can only be removed if they're redundant with a baseline earlier
# in the fallback order. They can never promoted up.
immediately_preceding_root = self._directories_immediately_preceding_root(baseline_name)
shared_result = None
root_baseline_unused = False
for directory in immediately_preceding_root:
this_result = new_results_by_directory.get(directory)
# If any of these directories don't have a baseline, there's no optimization we can do.
if not this_result:
return
if not shared_result:
shared_result = this_result
elif shared_result != this_result:
root_baseline_unused = True
baseline_root = self._baseline_root(baseline_name)
# The root baseline is unused if all the directories immediately preceding the root
# have a baseline, but have different baselines, so the baselines can't be promoted up.
if root_baseline_unused:
if baseline_root in new_results_by_directory:
del new_results_by_directory[baseline_root]
return
new_results_by_directory[baseline_root] = shared_result
for directory in immediately_preceding_root:
del new_results_by_directory[directory]
def _find_optimal_result_placement(self, baseline_name):
results_by_directory = self.read_results_by_directory(baseline_name)
results_by_port_name = self._results_by_port_name(results_by_directory, baseline_name)
new_results_by_directory = self._remove_redundant_results(
results_by_directory, results_by_port_name, baseline_name)
self._optimize_result_for_root(new_results_by_directory, baseline_name)
return results_by_directory, new_results_by_directory
def _remove_redundant_results(self, results_by_directory, results_by_port_name, baseline_name):
new_results_by_directory = copy.copy(results_by_directory)
for port_name, port in self._ports.items():
current_result = results_by_port_name.get(port_name)
# This happens if we're missing baselines for a port.
if not current_result:
continue
fallback_path = self._relative_baseline_search_paths(port, baseline_name)
current_index, current_directory = self._find_in_fallbackpath(fallback_path, current_result, new_results_by_directory)
for index in range(current_index + 1, len(fallback_path)):
new_directory = fallback_path[index]
if new_directory not in new_results_by_directory:
# No result for this baseline in this directory.
continue
elif new_results_by_directory[new_directory] == current_result:
# Result for new_directory are redundant with the result earlier in the fallback order.
if current_directory in new_results_by_directory:
del new_results_by_directory[current_directory]
else:
# The new_directory contains a different result, so stop trying to push results up.
break
return new_results_by_directory
def _find_in_fallbackpath(self, fallback_path, current_result, results_by_directory):
for index, directory in enumerate(fallback_path):
if directory in results_by_directory and (results_by_directory[directory] == current_result):
return index, directory
assert False, 'result %s not found in fallback_path %s, %s' % (current_result, fallback_path, results_by_directory)