| OLD | NEW |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """Functionality for adding TestExpectations lines and downloading baselines | 5 """A class for updating layout test expectations when updating w3c tests. |
| 6 based on layout test failures in try jobs. | |
| 7 | 6 |
| 8 This script is used as part of the w3c test auto-import process. | 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. |
| 9 """ | 12 """ |
| 10 | 13 |
| 11 import logging | 14 import logging |
| 12 | 15 |
| 13 from webkitpy.common.net.rietveld import Rietveld | |
| 14 from webkitpy.common.net.buildbot import BuildBot, Build | 16 from webkitpy.common.net.buildbot import BuildBot, Build |
| 15 from webkitpy.common.net.git_cl import GitCL | 17 from webkitpy.common.net.git_cl import GitCL |
| 18 from webkitpy.common.net.rietveld import Rietveld |
| 16 from webkitpy.common.webkit_finder import WebKitFinder | 19 from webkitpy.common.webkit_finder import WebKitFinder |
| 17 from webkitpy.w3c.test_parser import TestParser | 20 from webkitpy.w3c.test_parser import TestParser |
| 18 | 21 |
| 19 _log = logging.getLogger(__name__) | 22 _log = logging.getLogger(__name__) |
| 20 | 23 |
| 21 | 24 |
| 22 def main(host): | |
| 23 # TODO(qyearsley): Add a "main" function to W3CExpectationsLineAdder | |
| 24 # and move most or all of this logic in there. | |
| 25 host.initialize_scm() | |
| 26 port = host.port_factory.get() | |
| 27 expectations_file = port.path_to_generic_test_expectations_file() | |
| 28 expectations_line_adder = W3CExpectationsLineAdder(host) | |
| 29 issue_number = expectations_line_adder.get_issue_number() | |
| 30 try_bots = expectations_line_adder.get_try_bots() | |
| 31 rietveld = Rietveld(host.web) | |
| 32 try_jobs = rietveld.latest_try_jobs(issue_number, try_bots) | |
| 33 test_expectations = {} | |
| 34 if not try_jobs: | |
| 35 print 'No Try Job information was collected.' | |
| 36 return 1 | |
| 37 | |
| 38 for job in try_jobs: | |
| 39 platform_results = expectations_line_adder.get_failing_results_dict(Buil
dBot(), job.builder_name, job.build_number) | |
| 40 test_expectations = expectations_line_adder.merge_dicts(test_expectation
s, platform_results) | |
| 41 | |
| 42 for test_name, platform_result in test_expectations.iteritems(): | |
| 43 test_expectations[test_name] = expectations_line_adder.merge_same_valued
_keys(platform_result) | |
| 44 test_expectations = expectations_line_adder.get_expected_txt_files(test_expe
ctations) | |
| 45 test_expectation_lines = expectations_line_adder.create_line_list(test_expec
tations) | |
| 46 expectations_line_adder.write_to_test_expectations(host, expectations_file,
test_expectation_lines) | |
| 47 | |
| 48 | |
| 49 class W3CExpectationsLineAdder(object): | 25 class W3CExpectationsLineAdder(object): |
| 50 | 26 |
| 51 def __init__(self, host): | 27 def __init__(self, host): |
| 52 self._host = host | 28 self.host = host |
| 53 self.filesystem = host.filesystem | 29 self.host.initialize_scm() |
| 30 self.finder = WebKitFinder(self.host.filesystem) |
| 31 |
| 32 def run(self): |
| 33 issue_number = self.get_issue_number() |
| 34 try_bots = self.get_try_bots() |
| 35 rietveld = Rietveld(self.host.web) |
| 36 try_jobs = rietveld.latest_try_jobs(issue_number, try_bots) |
| 37 |
| 38 if not try_jobs: |
| 39 print 'No Try Job information was collected.' |
| 40 return 1 |
| 41 |
| 42 test_expectations = {} |
| 43 for job in try_jobs: |
| 44 platform_results = self.get_failing_results_dict(job) |
| 45 test_expectations = self.merge_dicts(test_expectations, platform_res
ults) |
| 46 |
| 47 for test_name, platform_result in test_expectations.iteritems(): |
| 48 test_expectations[test_name] = self.merge_same_valued_keys(platform_
result) |
| 49 |
| 50 test_expectations = self.get_expected_txt_files(test_expectations) |
| 51 test_expectation_lines = self.create_line_list(test_expectations) |
| 52 self.write_to_test_expectations(test_expectation_lines) |
| 53 return 0 |
| 54 | 54 |
| 55 def get_issue_number(self): | 55 def get_issue_number(self): |
| 56 return GitCL(self._host.executive).get_issue_number() | 56 return GitCL(self.host.executive).get_issue_number() |
| 57 | 57 |
| 58 def get_try_bots(self): | 58 def get_try_bots(self): |
| 59 return self._host.builders.all_try_builder_names() | 59 return self.host.builders.all_try_builder_names() |
| 60 | 60 |
| 61 def _generate_results_dict(self, platform, result_list): | 61 def generate_results_dict(self, platform, result_list): |
| 62 test_dict = {} | 62 test_dict = {} |
| 63 if '-' in platform: | 63 if '-' in platform: |
| 64 platform = platform[platform.find('-') + 1:].capitalize() | 64 platform = platform[platform.find('-') + 1:].capitalize() |
| 65 for result in result_list: | 65 for result in result_list: |
| 66 test_dict[result.test_name()] = { | 66 test_dict[result.test_name()] = { |
| 67 platform: { | 67 platform: { |
| 68 'expected': result.expected_results(), | 68 'expected': result.expected_results(), |
| 69 'actual': result.actual_results(), | 69 'actual': result.actual_results(), |
| 70 'bug': 'crbug.com/626703' | 70 'bug': 'crbug.com/626703' |
| 71 }} | 71 }} |
| 72 return test_dict | 72 return test_dict |
| 73 | 73 |
| 74 def get_failing_results_dict(self, buildbot, builder_name, build_number): | 74 def get_failing_results_dict(self, build): |
| 75 """Returns a nested dict of failing test results. | 75 """Returns a nested dict of failing test results. |
| 76 | 76 |
| 77 Retrieves a full list of layout test results from a builder result URL. | 77 Retrieves a full list of layout test results from a builder result URL. |
| 78 Collects the builder name, platform and a list of tests that did not | 78 Collects the builder name, platform and a list of tests that did not |
| 79 run as expected. | 79 run as expected. |
| 80 | 80 |
| 81 TODO(qyearsley): Rather than taking a BuildBot object, this should use | |
| 82 the Host's BuildBot object. | |
| 83 | |
| 84 Args: | 81 Args: |
| 85 buildbot: A BuildBot object. | |
| 86 builder: A Builder object. | |
| 87 build: A Build object. | 82 build: A Build object. |
| 88 | 83 |
| 89 Returns: | 84 Returns: |
| 90 A dictionary with the structure: { | 85 A dictionary with the structure: { |
| 91 'key': { | 86 'key': { |
| 92 'expected': 'TIMEOUT', | 87 'expected': 'TIMEOUT', |
| 93 'actual': 'CRASH', | 88 'actual': 'CRASH', |
| 94 'bug': 'crbug.com/11111' | 89 'bug': 'crbug.com/11111' |
| 95 } | 90 } |
| 96 } | 91 } |
| 97 If there are no failing results or no results could be fetched, | 92 If there are no failing results or no results could be fetched, |
| 98 this will return an empty dict. | 93 this will return an empty dict. |
| 99 """ | 94 """ |
| 100 layout_test_results = buildbot.fetch_results(Build(builder_name, build_n
umber)) | 95 layout_test_results = self.host.buildbot.fetch_results(build) |
| 101 if layout_test_results is None: | 96 if layout_test_results is None: |
| 102 _log.warning('No results for builder %s, build %s', builder_name, bu
ild_number) | 97 _log.warning('No results for build %s', build) |
| 103 return {} | 98 return {} |
| 104 platform = self._host.builders.port_name_for_builder_name(builder_name) | 99 platform = self.host.builders.port_name_for_builder_name(build.builder_n
ame) |
| 105 result_list = layout_test_results.didnt_run_as_expected_results() | 100 result_list = layout_test_results.didnt_run_as_expected_results() |
| 106 failing_results_dict = self._generate_results_dict(platform, result_list
) | 101 failing_results_dict = self.generate_results_dict(platform, result_list) |
| 107 return failing_results_dict | 102 return failing_results_dict |
| 108 | 103 |
| 109 def merge_dicts(self, target, source, path=None): | 104 def merge_dicts(self, target, source, path=None): |
| 110 """Recursively merges nested dictionaries. | 105 """Recursively merges nested dictionaries. |
| 111 | 106 |
| 112 Args: | 107 Args: |
| 113 target: First dictionary, which is updated based on source. | 108 target: First dictionary, which is updated based on source. |
| 114 source: Second dictionary, not modified. | 109 source: Second dictionary, not modified. |
| 115 | 110 |
| 116 Returns: | 111 Returns: |
| (...skipping 123 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 240 if isinstance(platform, tuple): | 235 if isinstance(platform, tuple): |
| 241 platform_list = list(platform) | 236 platform_list = list(platform) |
| 242 else: | 237 else: |
| 243 platform_list.append(platform) | 238 platform_list.append(platform) |
| 244 bug.append(platform_results[platform]['bug']) | 239 bug.append(platform_results[platform]['bug']) |
| 245 expectations = self.get_expectations(platform_results[platfo
rm]) | 240 expectations = self.get_expectations(platform_results[platfo
rm]) |
| 246 line = '%s [ %s ] %s [ %s ]' % (bug[0], ' '.join(platform_li
st), test_name, ' '.join(expectations)) | 241 line = '%s [ %s ] %s [ %s ]' % (bug[0], ' '.join(platform_li
st), test_name, ' '.join(expectations)) |
| 247 line_list.append(str(line)) | 242 line_list.append(str(line)) |
| 248 return line_list | 243 return line_list |
| 249 | 244 |
| 250 def write_to_test_expectations(self, host, path, line_list): | 245 def write_to_test_expectations(self, line_list): |
| 251 """Writes to TestExpectations. | 246 """Writes to TestExpectations. |
| 252 | 247 |
| 253 Writes the test expectations lines in |line_list| to the test | 248 Writes the test expectations lines in |line_list| to the test |
| 254 expectations file. | 249 expectations file. |
| 255 | 250 |
| 256 The place in the file where the new lines are inserted is after a | 251 The place in the file where the new lines are inserted is after a |
| 257 marker comment line. If this marker comment line is not found, it will | 252 marker comment line. If this marker comment line is not found, it will |
| 258 be added to the end of the file. | 253 be added to the end of the file. |
| 259 | 254 |
| 260 Args: | 255 Args: |
| 261 host: A Host object. | |
| 262 path: The path to the file general TestExpectations file. | |
| 263 line_list: A list of w3c test expectations lines. | 256 line_list: A list of w3c test expectations lines. |
| 264 """ | 257 """ |
| 258 port = self.host.port_factory.get() |
| 259 expectations_file = port.path_to_generic_test_expectations_file() |
| 265 comment_line = '# Tests added from W3C auto import bot' | 260 comment_line = '# Tests added from W3C auto import bot' |
| 266 file_contents = host.filesystem.read_text_file(path) | 261 file_contents = self.host.filesystem.read_text_file(expectations_file) |
| 267 w3c_comment_line_index = file_contents.find(comment_line) | 262 w3c_comment_line_index = file_contents.find(comment_line) |
| 268 all_lines = '' | 263 all_lines = '' |
| 269 for line in line_list: | 264 for line in line_list: |
| 270 end_bracket_index = line.split().index(']') | 265 end_bracket_index = line.split().index(']') |
| 271 test_name = line.split()[end_bracket_index + 1] | 266 test_name = line.split()[end_bracket_index + 1] |
| 272 if test_name in file_contents: | 267 if test_name in file_contents: |
| 273 continue | 268 continue |
| 274 all_lines += str(line) + '\n' | 269 all_lines += str(line) + '\n' |
| 275 all_lines = all_lines[:-1] | 270 all_lines = all_lines[:-1] |
| 276 if w3c_comment_line_index == -1: | 271 if w3c_comment_line_index == -1: |
| 277 file_contents += '\n%s\n' % comment_line | 272 file_contents += '\n%s\n' % comment_line |
| 278 file_contents += all_lines | 273 file_contents += all_lines |
| 279 else: | 274 else: |
| 280 end_of_comment_line = (file_contents[w3c_comment_line_index:].find('
\n')) + w3c_comment_line_index | 275 end_of_comment_line = (file_contents[w3c_comment_line_index:].find('
\n')) + w3c_comment_line_index |
| 281 new_data = file_contents[: end_of_comment_line + 1] + all_lines + fi
le_contents[end_of_comment_line:] | 276 new_data = file_contents[: end_of_comment_line + 1] + all_lines + fi
le_contents[end_of_comment_line:] |
| 282 file_contents = new_data | 277 file_contents = new_data |
| 283 host.filesystem.write_text_file(path, file_contents) | 278 self.host.filesystem.write_text_file(expectations_file, file_contents) |
| 284 | 279 |
| 285 def get_expected_txt_files(self, tests_results): | 280 def get_expected_txt_files(self, tests_results): |
| 286 """Fetches new baseline files for tests that sould be rebaselined. | 281 """Fetches new baseline files for tests that should be rebaselined. |
| 287 | 282 |
| 288 Invokes webkit-patch rebaseline-from-try-jobs in order to download new | 283 Invokes webkit-patch rebaseline-from-try-jobs in order to download new |
| 289 -expected.txt files for testharness.js tests that did not crash or time | 284 -expected.txt files for testharness.js tests that did not crash or time |
| 290 out. Then, the platform-specific test is removed from the overall | 285 out. Then, the platform-specific test is removed from the overall |
| 291 failure test dictionary. | 286 failure test dictionary. |
| 292 | 287 |
| 293 Args: | 288 Args: |
| 294 tests_results: A dictmapping test name to platform to test results. | 289 tests_results: A dict mapping test name to platform to test results. |
| 295 | 290 |
| 296 Returns: | 291 Returns: |
| 297 An updated tests_results dictionary without the platform-specific | 292 An updated tests_results dictionary without the platform-specific |
| 298 testharness.js tests that required new baselines to be downloaded | 293 testharness.js tests that required new baselines to be downloaded |
| 299 from `webkit-patch rebaseline-from-try-jobs`. | 294 from `webkit-patch rebaseline-from-try-jobs`. |
| 300 """ | 295 """ |
| 301 finder = WebKitFinder(self._host.filesystem) | 296 tests = self.host.executive.run_command(['git', 'diff', 'master', '--nam
e-only']).splitlines() |
| 302 tests = self._host.executive.run_command(['git', 'diff', 'master', '--na
me-only']).splitlines() | 297 tests_to_rebaseline, tests_results = self.get_tests_to_rebaseline(tests,
tests_results) |
| 303 tests_to_rebaseline, tests_results = self.get_tests_to_rebaseline(finder
, tests, tests_results) | |
| 304 if tests_to_rebaseline: | 298 if tests_to_rebaseline: |
| 305 webkit_patch = self._host.filesystem.join( | 299 webkit_patch = self.host.filesystem.join( |
| 306 finder.chromium_base(), finder.webkit_base(), finder.path_to_scr
ipt('webkit-patch')) | 300 self.finder.chromium_base(), self.finder.webkit_base(), self.fin
der.path_to_script('webkit-patch')) |
| 307 self._host.executive.run_command([ | 301 self.host.executive.run_command([ |
| 308 'python', | 302 'python', |
| 309 webkit_patch, | 303 webkit_patch, |
| 310 'rebaseline-cl', | 304 'rebaseline-cl', |
| 311 '--verbose', | 305 '--verbose', |
| 312 '--no-trigger-jobs', | 306 '--no-trigger-jobs', |
| 313 ] + tests_to_rebaseline) | 307 ] + tests_to_rebaseline) |
| 314 return tests_results | 308 return tests_results |
| 315 | 309 |
| 316 def get_tests_to_rebaseline(self, webkit_finder, tests, tests_results): | 310 def get_tests_to_rebaseline(self, tests, tests_results): |
| 317 """Returns a list of tests to download new baselines for. | 311 """Returns a list of tests to download new baselines for. |
| 318 | 312 |
| 319 Creates a list of tests to rebaseline depending on the tests' platform- | 313 Creates a list of tests to rebaseline depending on the tests' platform- |
| 320 specific results. In general, this will be non-ref tests that failed | 314 specific results. In general, this will be non-ref tests that failed |
| 321 due to a baseline mismatch (rather than crash or timeout). | 315 due to a baseline mismatch (rather than crash or timeout). |
| 322 | 316 |
| 323 Args: | 317 Args: |
| 324 webkit_finder: A WebKitFinder object. | |
| 325 tests: A list of new imported tests. | 318 tests: A list of new imported tests. |
| 326 tests_results: A dictionary of failing tests results. | 319 tests_results: A dictionary of failing tests results. |
| 327 | 320 |
| 328 Returns: | 321 Returns: |
| 329 A pair: A set of tests to be rebaselined, and an updated | 322 A pair: A set of tests to be rebaselined, and an updated |
| 330 tests_results dictionary. These tests to be rebaselined includes | 323 tests_results dictionary. These tests to be rebaselined includes |
| 331 both testharness.js tests and ref tests that failed some try job. | 324 both testharness.js tests and ref tests that failed some try job. |
| 332 """ | 325 """ |
| 333 tests_to_rebaseline = set() | 326 tests_to_rebaseline = set() |
| 334 layout_tests_rel_path = self._host.filesystem.relpath( | 327 layout_tests_rel_path = self.host.filesystem.relpath( |
| 335 webkit_finder.layout_tests_dir(), webkit_finder.chromium_base()) | 328 self.finder.layout_tests_dir(), self.finder.chromium_base()) |
| 336 for test in tests: | 329 for test in tests: |
| 337 test_path = self._host.filesystem.relpath(test, layout_tests_rel_pat
h) | 330 test_path = self.host.filesystem.relpath(test, layout_tests_rel_path
) |
| 338 if self.is_js_test(webkit_finder, test) and tests_results.get(test_p
ath): | 331 if self.is_js_test(test) and tests_results.get(test_path): |
| 339 for platform in tests_results[test_path].keys(): | 332 for platform in tests_results[test_path].keys(): |
| 340 if tests_results[test_path][platform]['actual'] not in ['CRA
SH', 'TIMEOUT']: | 333 if tests_results[test_path][platform]['actual'] not in ['CRA
SH', 'TIMEOUT']: |
| 341 del tests_results[test_path][platform] | 334 del tests_results[test_path][platform] |
| 342 tests_to_rebaseline.add(test_path) | 335 tests_to_rebaseline.add(test_path) |
| 343 return list(tests_to_rebaseline), tests_results | 336 return list(tests_to_rebaseline), tests_results |
| 344 | 337 |
| 345 def is_js_test(self, webkit_finder, test_path): | 338 def is_js_test(self, test_path): |
| 346 absolute_path = self._host.filesystem.join(webkit_finder.chromium_base()
, test_path) | 339 absolute_path = self.host.filesystem.join(self.finder.chromium_base(), t
est_path) |
| 347 test_parser = TestParser(absolute_path, self._host) | 340 test_parser = TestParser(absolute_path, self.host) |
| 348 return test_parser.is_jstest() | 341 return test_parser.is_jstest() |
| OLD | NEW |