Clean up ChunkedUpdateDrawingAreaProxy
[WebKit-https.git] / WebKitTools / Scripts / webkitpy / layout_tests / deduplicate_tests.py
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
6 # are met:
7 #
8 # 1.  Redistributions of source code must retain the above copyright
9 #     notice, this list of conditions and the following disclaimer.
10 # 2.  Redistributions in binary form must reproduce the above copyright
11 #     notice, this list of conditions and the following disclaimer in the
12 #     documentation and/or other materials provided with the distribution.
13 #
14 # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
15 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17 # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
18 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
21 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
23 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24
25 """deduplicate_tests -- lists duplicated between platforms.
26
27 If platform/mac-leopard is missing an expected test output, we fall back on
28 platform/mac.  This means it's possible to grow redundant test outputs,
29 where we have the same expected data in both a platform directory and another
30 platform it falls back on.
31 """
32
33 import collections
34 import fnmatch
35 import os
36 import subprocess
37 import sys
38 import re
39 import webkitpy.common.checkout.scm as scm
40 import webkitpy.common.system.executive as executive
41 import webkitpy.common.system.logutils as logutils
42 import webkitpy.common.system.ospath as ospath
43 import webkitpy.layout_tests.port.factory as port_factory
44
45 _log = logutils.get_logger(__file__)
46
47 _BASE_PLATFORM = 'base'
48
49
50 def port_fallbacks():
51     """Get the port fallback information.
52     Returns:
53         A dictionary mapping platform name to a list of other platforms to fall
54         back on.  All platforms fall back on 'base'.
55     """
56     fallbacks = {_BASE_PLATFORM: []}
57     platform_dir = os.path.join(scm.find_checkout_root(), 'LayoutTests',
58                                 'platform')
59     for port_name in os.listdir(platform_dir):
60         try:
61             platforms = port_factory.get(port_name).baseline_search_path()
62         except NotImplementedError:
63             _log.error("'%s' lacks baseline_search_path(), please fix."
64                        % port_name)
65             fallbacks[port_name] = [_BASE_PLATFORM]
66             continue
67         fallbacks[port_name] = [os.path.basename(p) for p in platforms][1:]
68         fallbacks[port_name].append(_BASE_PLATFORM)
69     return fallbacks
70
71
72 def parse_git_output(git_output, glob_pattern):
73     """Parses the output of git ls-tree and filters based on glob_pattern.
74     Args:
75         git_output: result of git ls-tree -r HEAD LayoutTests.
76         glob_pattern: a pattern to filter the files.
77     Returns:
78         A dictionary mapping (test name, hash of content) => [paths]
79     """
80     hashes = collections.defaultdict(set)
81     for line in git_output.split('\n'):
82         if not line:
83             break
84         attrs, path = line.strip().split('\t')
85         if not fnmatch.fnmatch(path, glob_pattern):
86             continue
87         path = path[len('LayoutTests/'):]
88         match = re.match(r'^(platform/.*?/)?(.*)', path)
89         test = match.group(2)
90         _, _, hash = attrs.split(' ')
91         hashes[(test, hash)].add(path)
92     return hashes
93
94
95 def cluster_file_hashes(glob_pattern):
96     """Get the hashes of all the test expectations in the tree.
97     We cheat and use git's hashes.
98     Args:
99         glob_pattern: a pattern to filter the files.
100     Returns:
101         A dictionary mapping (test name, hash of content) => [paths]
102     """
103
104     # A map of file hash => set of all files with that hash.
105     hashes = collections.defaultdict(set)
106
107     # Fill in the map.
108     cmd = ('git', 'ls-tree', '-r', 'HEAD', 'LayoutTests')
109     try:
110         git_output = executive.Executive().run_command(cmd,
111             cwd=scm.find_checkout_root())
112     except OSError, e:
113         if e.errno == 2:  # No such file or directory.
114             _log.error("Error: 'No such file' when running git.")
115             _log.error("This script requires git.")
116             sys.exit(1)
117         raise e
118     return parse_git_output(git_output, glob_pattern)
119
120
121 def extract_platforms(paths):
122     """Extracts the platforms from a list of paths matching ^platform/(.*?)/.
123     Args:
124         paths: a list of paths.
125     Returns:
126         A dictionary containing all platforms from paths.
127     """
128     platforms = {}
129     for path in paths:
130         match = re.match(r'^platform/(.*?)/', path)
131         if match:
132             platform = match.group(1)
133         else:
134             platform = _BASE_PLATFORM
135         platforms[platform] = path
136     return platforms
137
138
139 def has_intermediate_results(test, fallbacks, matching_platform,
140                              path_exists=os.path.exists):
141     """Returns True if there is a test result that causes us to not delete
142     this duplicate.
143
144     For example, chromium-linux may be a duplicate of the checked in result,
145     but chromium-win may have a different result checked in.  In this case,
146     we need to keep the duplicate results.
147
148     Args:
149         test: The test name.
150         fallbacks: A list of platforms we fall back on.
151         matching_platform: The platform that we found the duplicate test
152             result.  We can stop checking here.
153         path_exists: Optional parameter that allows us to stub out
154             os.path.exists for testing.
155     """
156     for platform in fallbacks:
157         if platform == matching_platform:
158             return False
159         test_path = os.path.join('LayoutTests', 'platform', platform, test)
160         if path_exists(test_path):
161             return True
162     return False
163
164
165 def get_relative_test_path(filename, relative_to,
166                            checkout_root=scm.find_checkout_root()):
167     """Constructs a relative path to |filename| from |relative_to|.
168     Args:
169         filename: The test file we're trying to get a relative path to.
170         relative_to: The absolute path we're relative to.
171     Returns:
172         A relative path to filename or None if |filename| is not below
173         |relative_to|.
174     """
175     layout_test_dir = os.path.join(checkout_root, 'LayoutTests')
176     abs_path = os.path.join(layout_test_dir, filename)
177     return ospath.relpath(abs_path, relative_to)
178
179
180 def find_dups(hashes, port_fallbacks, relative_to):
181     """Yields info about redundant test expectations.
182     Args:
183         hashes: a list of hashes as returned by cluster_file_hashes.
184         port_fallbacks: a list of fallback information as returned by
185             get_port_fallbacks.
186         relative_to: the directory that we want the results relative to
187     Returns:
188         a tuple containing (test, platform, fallback, platforms)
189     """
190     for (test, hash), cluster in hashes.items():
191         if len(cluster) < 2:
192             continue  # Common case: only one file with that hash.
193
194         # Compute the list of platforms we have this particular hash for.
195         platforms = extract_platforms(cluster)
196         if len(platforms) == 1:
197             continue
198
199         # See if any of the platforms are redundant with each other.
200         for platform in platforms.keys():
201             for fallback in port_fallbacks[platform]:
202                 if fallback not in platforms.keys():
203                     continue
204                 # We have to verify that there isn't an intermediate result
205                 # that causes this duplicate hash to exist.
206                 if has_intermediate_results(test, port_fallbacks[platform],
207                                             fallback):
208                     continue
209                 # We print the relative path so it's easy to pipe the results
210                 # to xargs rm.
211                 path = get_relative_test_path(platforms[platform], relative_to)
212                 if not path:
213                     continue
214                 yield {
215                     'test': test,
216                     'platform': platform,
217                     'fallback': fallback,
218                     'path': path,
219                 }
220
221
222 def deduplicate(glob_pattern):
223     """Traverses LayoutTests and returns information about duplicated files.
224     Args:
225         glob pattern to filter the files in LayoutTests.
226     Returns:
227         a dictionary containing test, path, platform and fallback.
228     """
229     fallbacks = port_fallbacks()
230     hashes = cluster_file_hashes(glob_pattern)
231     return list(find_dups(hashes, fallbacks, os.getcwd()))