Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(87)

Side by Side Diff: third_party/WebKit/Tools/Scripts/webkitpy/w3c/update_w3c_test_expectations.py

Issue 2652653011: Rename update_w3c_test_expectations and related files. (Closed)
Patch Set: Rebase, add back accidentally-removed "directory owner extractor" code Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 # Copyright 2016 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 """A class for updating layout test expectations when updating w3c tests.
6
7 Specifically, this class fetches results from try bots for the current CL, and:
8 1. Downloads new baseline files for any tests that can be rebaselined.
9 2. Updates the generic TestExpectations file for any other failing tests.
10
11 This is used as part of the w3c test auto-import process.
12 """
13
14 import argparse
15 import copy
16 import logging
17
18 from webkitpy.common.net.git_cl import GitCL
19 from webkitpy.common.net.rietveld import Rietveld
20 from webkitpy.common.webkit_finder import WebKitFinder
21 from webkitpy.layout_tests.models.test_expectations import TestExpectationLine
22 from webkitpy.w3c.test_parser import TestParser
23
24 _log = logging.getLogger(__name__)
25
26 MARKER_COMMENT = '# ====== New tests from w3c-test-autoroller added here ======'
27
28
29 class W3CExpectationsLineAdder(object):
30
31 def __init__(self, host):
32 self.host = host
33 self.host.initialize_scm()
34 self.finder = WebKitFinder(self.host.filesystem)
35
36 def run(self, args=None):
37 """Downloads text new baselines and adds test expectations lines."""
38 parser = argparse.ArgumentParser(description=__doc__)
39 parser.add_argument('-v', '--verbose', action='store_true', help='More v erbose logging.')
40 args = parser.parse_args(args)
41
42 log_level = logging.DEBUG if args.verbose else logging.INFO
43 logging.basicConfig(level=log_level, format='%(message)s')
44
45 issue_number = self.get_issue_number()
46 if issue_number == 'None':
47 _log.error('No issue on current branch.')
48 return 1
49
50 rietveld = Rietveld(self.host.web)
51 builds = rietveld.latest_try_jobs(issue_number, self.get_try_bots())
52 _log.debug('Latest try jobs: %r', builds)
53 if not builds:
54 _log.error('No try job information was collected.')
55 return 1
56
57 # Here we build up a dict of failing test results for all platforms.
58 test_expectations = {}
59 for build in builds:
60 platform_results = self.get_failing_results_dict(build)
61 test_expectations = self.merge_dicts(test_expectations, platform_res ults)
62
63 # And then we merge results for different platforms that had the same re sults.
64 for test_name, platform_result in test_expectations.iteritems():
65 # platform_result is a dict mapping platforms to results.
66 test_expectations[test_name] = self.merge_same_valued_keys(platform_ result)
67
68 test_expectations = self.download_text_baselines(test_expectations)
69 test_expectation_lines = self.create_line_list(test_expectations)
70 self.write_to_test_expectations(test_expectation_lines)
71 return 0
72
73 def get_issue_number(self):
74 """Returns current CL number. Can be replaced in unit tests."""
75 return GitCL(self.host).get_issue_number()
76
77 def get_try_bots(self):
78 """Returns try bot names. Can be replaced in unit tests."""
79 return self.host.builders.all_try_builder_names()
80
81 def get_failing_results_dict(self, build):
82 """Returns a nested dict of failing test results.
83
84 Retrieves a full list of layout test results from a builder result URL.
85 Collects the builder name, platform and a list of tests that did not
86 run as expected.
87
88 Args:
89 build: A Build object.
90
91 Returns:
92 A dictionary with the structure: {
93 'key': {
94 'expected': 'TIMEOUT',
95 'actual': 'CRASH',
96 'bug': 'crbug.com/11111'
97 }
98 }
99 If there are no failing results or no results could be fetched,
100 this will return an empty dict.
101 """
102 layout_test_results = self.host.buildbot.fetch_results(build)
103 if layout_test_results is None:
104 _log.warning('No results for build %s', build)
105 return {}
106 platform = self.host.builders.port_name_for_builder_name(build.builder_n ame)
107 test_results = layout_test_results.didnt_run_as_expected_results()
108 failing_results_dict = self.generate_results_dict(platform, test_results )
109 return failing_results_dict
110
111 def generate_results_dict(self, full_port_name, test_results):
112 """Makes a dict with results for one platform.
113
114 Args:
115 full_port_name: The full port name, e.g. "win-win10".
116 test_results: A list of LayoutTestResult objects.
117
118 Returns:
119 A dict mapping to platform string (e.g. "Win10") to a dict with
120 the results for that test and that platform.
121 """
122 platform = self._port_name_to_platform_specifier(full_port_name)
123 test_dict = {}
124 for result in test_results:
125 test_dict[result.test_name()] = {
126 platform: {
127 'expected': result.expected_results(),
128 'actual': result.actual_results(),
129 'bug': 'crbug.com/626703'
130 }}
131 return test_dict
132
133 def _port_name_to_platform_specifier(self, port_name):
134 """Maps a port name to the string used in test expectations lines.
135
136 For example:
137 linux-trusty -> Trusty
138 mac-mac10.11 -> Mac10.11.
139 """
140 # TODO(qyearsley): Do this in a more robust way with Port classes.
141 if '-' in port_name:
142 return port_name[port_name.find('-') + 1:].capitalize()
143 return port_name
144
145 def merge_dicts(self, target, source, path=None):
146 """Recursively merges nested dictionaries.
147
148 Args:
149 target: First dictionary, which is updated based on source.
150 source: Second dictionary, not modified.
151
152 Returns:
153 An updated target dictionary.
154 """
155 path = path or []
156 for key in source:
157 if key in target:
158 if (isinstance(target[key], dict)) and isinstance(source[key], d ict):
159 self.merge_dicts(target[key], source[key], path + [str(key)] )
160 elif target[key] == source[key]:
161 pass
162 else:
163 raise ValueError('The key: %s already exist in the target di ctionary.' % '.'.join(path))
164 else:
165 target[key] = source[key]
166 return target
167
168 def merge_same_valued_keys(self, dictionary):
169 """Merges keys in dictionary with same value.
170
171 Traverses through a dict and compares the values of keys to one another.
172 If the values match, the keys are combined to a tuple and the previous
173 keys are removed from the dict.
174
175 Args:
176 dictionary: A dictionary with a dictionary as the value.
177
178 Returns:
179 A new dictionary with updated keys to reflect matching values of key s.
180 Example: {
181 'one': {'foo': 'bar'},
182 'two': {'foo': 'bar'},
183 'three': {'foo': 'bar'}
184 }
185 is converted to a new dictionary with that contains
186 {('one', 'two', 'three'): {'foo': 'bar'}}
187 """
188 merged_dict = {}
189 matching_value_keys = set()
190 keys = sorted(dictionary.keys())
191 while keys:
192 current_key = keys[0]
193 found_match = False
194 if current_key == keys[-1]:
195 merged_dict[current_key] = dictionary[current_key]
196 keys.remove(current_key)
197 break
198
199 for next_item in keys[1:]:
200 if dictionary[current_key] == dictionary[next_item]:
201 found_match = True
202 matching_value_keys.update([current_key, next_item])
203
204 if next_item == keys[-1]:
205 if found_match:
206 merged_dict[tuple(matching_value_keys)] = dictionary[cur rent_key]
207 keys = [k for k in keys if k not in matching_value_keys]
208 else:
209 merged_dict[current_key] = dictionary[current_key]
210 keys.remove(current_key)
211 matching_value_keys = set()
212 return merged_dict
213
214 def get_expectations(self, results):
215 """Returns a set of test expectations for a given test dict.
216
217 Returns a set of one or more test expectations based on the expected
218 and actual results of a given test name.
219
220 Args:
221 results: A dictionary that maps one test to its results. Example:
222 {
223 'test_name': {
224 'expected': 'PASS',
225 'actual': 'FAIL',
226 'bug': 'crbug.com/11111'
227 }
228 }
229
230 Returns:
231 A set of one or more test expectation strings with the first letter
232 capitalized. Example: set(['Failure', 'Timeout']).
233 """
234 expectations = set()
235 failure_types = ['TEXT', 'FAIL', 'IMAGE+TEXT', 'IMAGE', 'AUDIO', 'MISSIN G', 'LEAK']
236 test_expectation_types = ['SLOW', 'TIMEOUT', 'CRASH', 'PASS', 'REBASELIN E', 'NEEDSREBASELINE', 'NEEDSMANUALREBASELINE']
237 for expected in results['expected'].split():
238 for actual in results['actual'].split():
239 if expected in test_expectation_types and actual in failure_type s:
240 expectations.add('Failure')
241 if expected in failure_types and actual in test_expectation_type s:
242 expectations.add(actual.capitalize())
243 if expected in test_expectation_types and actual in test_expecta tion_types:
244 expectations.add(actual.capitalize())
245 return expectations
246
247 def create_line_list(self, merged_results):
248 """Creates list of test expectations lines.
249
250 Traverses through the given |merged_results| dictionary and parses the
251 value to create one test expectations line per key.
252
253 Args:
254 merged_results: A merged_results with the format:
255 {
256 'test_name': {
257 'platform': {
258 'expected: 'PASS',
259 'actual': 'FAIL',
260 'bug': 'crbug.com/11111'
261 }
262 }
263 }
264
265 Returns:
266 A list of test expectations lines with the format:
267 ['BUG_URL [PLATFORM(S)] TEST_MAME [EXPECTATION(S)]']
268 """
269 line_list = []
270 for test_name, platform_results in merged_results.iteritems():
271 for platform in platform_results:
272 if test_name.startswith('external'):
273 platform_list = []
274 bug = []
275 expectations = []
276 if isinstance(platform, tuple):
277 platform_list = list(platform)
278 else:
279 platform_list.append(platform)
280 bug.append(platform_results[platform]['bug'])
281 expectations = self.get_expectations(platform_results[platfo rm])
282 line = '%s [ %s ] %s [ %s ]' % (bug[0], ' '.join(platform_li st), test_name, ' '.join(expectations))
283 line_list.append(str(line))
284 return line_list
285
286 def write_to_test_expectations(self, line_list):
287 """Writes to TestExpectations.
288
289 The place in the file where the new lines are inserted is after a
290 marker comment line. If this marker comment line is not found, it will
291 be added to the end of the file.
292
293 Args:
294 line_list: A list of lines to add to the TestExpectations file.
295 """
296 _log.info('Lines to write to TestExpectations:')
297 for line in line_list:
298 _log.info(' %s', line)
299 port = self.host.port_factory.get()
300 expectations_file_path = port.path_to_generic_test_expectations_file()
301 file_contents = self.host.filesystem.read_text_file(expectations_file_pa th)
302 marker_comment_index = file_contents.find(MARKER_COMMENT)
303 line_list = [line for line in line_list if self._test_name_from_expectat ion_string(line) not in file_contents]
304 if not line_list:
305 return
306 if marker_comment_index == -1:
307 file_contents += '\n%s\n' % MARKER_COMMENT
308 file_contents += '\n'.join(line_list)
309 else:
310 end_of_marker_line = (file_contents[marker_comment_index:].find('\n' )) + marker_comment_index
311 file_contents = file_contents[:end_of_marker_line + 1] + '\n'.join(l ine_list) + file_contents[end_of_marker_line:]
312 self.host.filesystem.write_text_file(expectations_file_path, file_conten ts)
313
314 @staticmethod
315 def _test_name_from_expectation_string(expectation_string):
316 return TestExpectationLine.tokenize_line(filename='', expectation_string =expectation_string, line_number=0).name
317
318 def download_text_baselines(self, tests_results):
319 """Fetches new baseline files for tests that should be rebaselined.
320
321 Invokes `webkit-patch rebaseline-cl` in order to download new baselines
322 (-expected.txt files) for testharness.js tests that did not crash or
323 time out. Then, the platform-specific test is removed from the overall
324 failure test dictionary.
325
326 Args:
327 tests_results: A dict mapping test name to platform to test results.
328
329 Returns:
330 An updated tests_results dictionary without the platform-specific
331 testharness.js tests that required new baselines to be downloaded
332 from `webkit-patch rebaseline-cl`.
333 """
334 tests_to_rebaseline, tests_results = self.get_tests_to_rebaseline(tests_ results)
335 _log.info('Tests to rebaseline:')
336 for test in tests_to_rebaseline:
337 _log.info(' %s', test)
338 if tests_to_rebaseline:
339 webkit_patch = self.host.filesystem.join(
340 self.finder.chromium_base(), self.finder.webkit_base(), self.fin der.path_to_script('webkit-patch'))
341 self.host.executive.run_command([
342 'python',
343 webkit_patch,
344 'rebaseline-cl',
345 '--verbose',
346 '--no-trigger-jobs',
347 ] + tests_to_rebaseline)
348 return tests_results
349
350 def get_tests_to_rebaseline(self, test_results):
351 """Returns a list of tests to download new baselines for.
352
353 Creates a list of tests to rebaseline depending on the tests' platform-
354 specific results. In general, this will be non-ref tests that failed
355 due to a baseline mismatch (rather than crash or timeout).
356
357 Args:
358 test_results: A dictionary of failing test results, mapping tests
359 to platforms to result dicts.
360
361 Returns:
362 A pair: A set of tests to be rebaselined, and a modified copy of
363 the test results dictionary. The tests to be rebaselined should
364 include testharness.js tests that failed due to a baseline mismatch.
365 """
366 test_results = copy.deepcopy(test_results)
367 tests_to_rebaseline = set()
368 for test_path in test_results:
369 if not (self.is_js_test(test_path) and test_results.get(test_path)):
370 continue
371 for platform in test_results[test_path].keys():
372 if test_results[test_path][platform]['actual'] not in ['CRASH', 'TIMEOUT']:
373 del test_results[test_path][platform]
374 tests_to_rebaseline.add(test_path)
375 return sorted(tests_to_rebaseline), test_results
376
377 def is_js_test(self, test_path):
378 """Checks whether a given file is a testharness.js test.
379
380 Args:
381 test_path: A file path relative to the layout tests directory.
382 This might correspond to a deleted file or a non-test.
383 """
384 absolute_path = self.host.filesystem.join(self.finder.layout_tests_dir() , test_path)
385 test_parser = TestParser(absolute_path, self.host)
386 if not test_parser.test_doc:
387 return False
388 return test_parser.is_jstest()
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698