Chromium Code Reviews| OLD | NEW |
|---|---|
| (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 script to modify TestExpectations lines based layout test failures in try j obs. | |
| 6 | |
| 7 This script outputs a list of test expectation lines to add to a 'TestExpectatio ns' file | |
| 8 by retrieving the try job results for the current CL. | |
| 9 """ | |
| 10 | |
| 11 import logging | |
| 12 | |
| 13 from webkitpy.common.net import buildbot | |
| 14 from webkitpy.common.net import rietveld | |
| 15 | |
| 16 | |
| 17 _log = logging.getLogger(__name__) | |
| 18 | |
| 19 | |
| 20 def main(host, port): | |
| 21 expectations_file = port.path_to_generic_test_expectations_file() | |
| 22 expectations_line_adder = W3CExpectationsLineAdder(host) | |
| 23 issue_number = expectations_line_adder.get_issue_number() | |
| 24 try_bots = expectations_line_adder.get_try_bots() | |
| 25 try_jobs = rietveld.latest_try_jobs(issue_number, try_bots, host.web) | |
| 26 line_expectations_dict = {} | |
| 27 if not try_jobs: | |
| 28 print 'No Try Job information was collected.' | |
| 29 return 1 | |
| 30 for try_job in try_jobs: | |
| 31 builder_name = try_job[0] | |
| 32 build_number = try_job[1] | |
| 33 builder = buildbot.Builder(builder_name, host.buildbot) | |
| 34 build = buildbot.Build(builder, build_number) | |
| 35 platform_results_dict = expectations_line_adder.get_failing_results_dict (builder, build) | |
| 36 line_expectations_dict = expectations_line_adder.merge_dicts(line_expect ations_dict, platform_results_dict) | |
| 37 for platform_results_dicts in line_expectations_dict.values(): | |
| 38 platform_results_dicts = expectations_line_adder.merge_same_valued_keys( platform_results_dicts) | |
| 39 line_list = expectations_line_adder.create_line_list(line_expectations_dict) | |
| 40 expectations_line_adder.write_to_test_expectations(host, expectations_file, line_list) | |
| 41 | |
| 42 | |
| 43 class W3CExpectationsLineAdder(object): | |
| 44 | |
| 45 def __init__(self, host): | |
| 46 self._host = host | |
| 47 | |
| 48 def get_issue_number(self): | |
| 49 return self._host._scm.get_issue_number() | |
|
qyearsley
2016/07/20 16:55:28
This line is accessing a private attribute of anot
dcampb
2016/07/20 21:11:59
done
| |
| 50 | |
| 51 def get_try_bots(self): | |
| 52 return self._host.builders.all_try_builder_names() | |
| 53 | |
| 54 def _generate_results_dict(self, platform, result_list): | |
| 55 test_dict = {} | |
| 56 if '-' in platform: | |
| 57 platform = platform[platform.find('-') + 1:].capitalize() | |
| 58 for result in result_list: | |
| 59 test_dict[result.test_name()] = { | |
| 60 platform: { | |
| 61 'expected': result.expected_results(), | |
| 62 'actual': result.actual_results(), | |
| 63 'bug': 'crbug.com/626703' | |
| 64 }} | |
| 65 return test_dict | |
| 66 | |
| 67 def get_failing_results_dict(self, builder, build): | |
| 68 """Returns a nested dict of failing test results. | |
| 69 | |
| 70 Retrieves a full list of layout test results from a builder result URL. Collects | |
| 71 the builder name, platform and a list of tests that did not run as expec ted. | |
| 72 | |
| 73 Args: | |
| 74 builder: A Builder object. | |
| 75 build: A Build object. | |
| 76 | |
| 77 Returns: | |
| 78 A dictionary with the structure: { | |
| 79 'key': { | |
| 80 'expected': 'TIMEOUT', | |
| 81 'actual': 'CRASH', | |
| 82 'bug': 'crbug.com/11111' | |
| 83 } | |
| 84 } | |
| 85 """ | |
| 86 layout_test_results = builder.fetch_layout_test_results(build.results_ur l()) | |
| 87 builder_name = layout_test_results.builder_name() | |
| 88 platform = self._host.builders.port_name_for_builder_name(builder_name) | |
| 89 result_list = layout_test_results.didnt_run_as_expected_results() | |
| 90 failing_results_dict = self._generate_results_dict(platform, result_list ) | |
| 91 return failing_results_dict | |
| 92 | |
| 93 def merge_dicts(self, final, temp, path=None): | |
|
qyearsley
2016/07/20 16:55:27
A docstring could also be useful here. saying some
dcampb
2016/07/20 21:11:59
I chose the first option, I don't really see a ben
qyearsley
2016/07/20 21:21:45
Sounds good -- and with the docstring and argument
| |
| 94 path = path or [] | |
| 95 for key in temp: | |
| 96 if key in final: | |
| 97 if (isinstance(final[key], dict)) and isinstance(temp[key], dict ): | |
| 98 self.merge_dicts(final[key], temp[key], path + [str(key)]) | |
| 99 elif final[key] == temp[key]: | |
| 100 pass | |
| 101 else: | |
| 102 raise Exception('conflict at %s' % '.'.join(path)) | |
|
qyearsley
2016/07/20 16:55:28
Usually it's better to raise a more specific excep
dcampb
2016/07/20 21:11:59
Changed it to a value error, with the conflict lin
| |
| 103 else: | |
| 104 final[key] = temp[key] | |
| 105 return final | |
| 106 | |
| 107 def merge_same_valued_keys(self, dictionary): | |
| 108 """Merges keys in dictionary with same value. | |
| 109 | |
| 110 Traverses through a dict and compares the values of keys to one another. | |
| 111 If the values match, the keys are combined to a tuple and the previous k eys | |
| 112 are removed from the dict. | |
| 113 | |
| 114 Args: | |
| 115 dictionary: A dictionary with a dictionary as the value. | |
| 116 | |
| 117 Returns: | |
| 118 A dictionary with updated keys to reflect matching values of keys. | |
| 119 Example: { | |
| 120 'one': {'foo': 'bar'}, | |
| 121 'two': {'foo': 'bar'}, | |
| 122 'three': {'foo': bar'} | |
| 123 } is converted to | |
|
qyearsley
2016/07/20 16:55:28
Indentation is off here -- maybe due to tab charac
dcampb
2016/07/20 21:11:59
fixed.
| |
| 124 {('one', 'two', 'three'): {'foo': 'bar'}} | |
| 125 """ | |
| 126 matching_value_keys = set() | |
| 127 keys = dictionary.keys() | |
| 128 is_last_item = False | |
| 129 for index, item in enumerate(keys): | |
| 130 if is_last_item: | |
| 131 break | |
| 132 for i in range(index + 1, len(keys)): | |
| 133 next_item = keys[i] | |
| 134 if dictionary[item] == dictionary[next_item]: | |
| 135 matching_value_keys.update([item, next_item]) | |
| 136 dictionary[tuple(matching_value_keys)] = dictionary[item] | |
| 137 is_last_item = next_item == keys[-1] | |
| 138 del dictionary[item] | |
| 139 del dictionary[next_item] | |
| 140 return dictionary | |
| 141 | |
| 142 def get_expectations(self, results): | |
| 143 '''Returns a list of test expectations for a given test dict. | |
|
qyearsley
2016/07/20 16:55:27
Use double-quotes (""") for docstrings for consist
dcampb
2016/07/20 21:11:59
fixed.
dcampb
2016/07/20 21:11:59
fixed.
dcampb
2016/07/20 21:11:59
fixed.
| |
| 144 | |
| 145 Returns a list of one or more test expectations based on the expected | |
| 146 and actual results of a given test name. | |
| 147 | |
| 148 Args: | |
| 149 results: A dictionary that maps one test to its results. Example: { | |
| 150 'test_name': { | |
| 151 'expected': 'PASS', | |
| 152 'actual': 'FAIL', | |
| 153 'bug': 'crbug.com/11111' | |
| 154 } | |
| 155 } | |
| 156 | |
| 157 Returns: | |
| 158 A list of one or more test expectations with the first letter capita lized. Example: | |
| 159 ['Failure', 'Timeout'] | |
| 160 ''' | |
| 161 expectations = [] | |
| 162 failure_expectations = ['TEXT', 'FAIL', 'IMAGE+TEXT', 'IMAGE'] | |
| 163 pass_crash_timeout = ['TIMEOUT', 'CRASH', 'PASS'] | |
| 164 if results['expected'] in pass_crash_timeout and results['actual'] in fa ilure_expectations: | |
| 165 expectations.append('Failure') | |
| 166 if results['expected'] in failure_expectations and results['actual'] in pass_crash_timeout: | |
| 167 expectations.append(results['actual'].capitalize()) | |
| 168 if results['expected'] in pass_crash_timeout and results['actual'] in pa ss_crash_timeout: | |
| 169 expectations.append(results['actual'].capitalize()) | |
| 170 expectations.append(results['expected'].capitalize()) | |
| 171 return expectations | |
| 172 | |
| 173 def create_line_list(self, merged_results): | |
| 174 """Creates list of test expectations lines. | |
| 175 | |
| 176 Traverses through a merged_results and parses the value to create a test | |
| 177 expectations line per key. | |
| 178 | |
| 179 Args: | |
| 180 merged_results: A merged_results with the format { | |
| 181 'test_name': { | |
| 182 'platform': { | |
| 183 'expected: 'PASS', | |
| 184 'actual': 'FAIL', | |
| 185 'bug': 'crbug.com/11111' | |
| 186 } | |
| 187 } | |
| 188 } | |
| 189 It is possible for the dicitonary to have many test_name | |
| 190 keys. | |
| 191 | |
| 192 Returns: | |
| 193 A list of test expectations lines with the format | |
| 194 ['BUG_URL [PLATFORM(S)] TEST_MAME [EXPECTATION(S)]'] | |
| 195 """ | |
| 196 line_list = [] | |
| 197 for test_name, platform_results in merged_results.iteritems(): | |
| 198 for platform in platform_results: | |
| 199 platform_list = [] | |
| 200 bug = [] | |
| 201 expectations = [] | |
| 202 if isinstance(platform, tuple): | |
| 203 platform_list = list(platform) | |
| 204 else: | |
| 205 platform_list.append(platform) | |
| 206 bug.append(platform_results[platform]['bug']) | |
| 207 expectations = self.get_expectations(platform_results[platform]) | |
| 208 line = '%s [ %s ] %s [ %s ]' % (bug[0], ' '.join(platform_list), test_name, ' '.join(expectations)) | |
| 209 line_list.append(str(line)) | |
| 210 return line_list | |
| 211 | |
| 212 def write_to_test_expectations(self, host, path, line_list): | |
| 213 """Writes to TestExpectations. | |
| 214 | |
| 215 Writes to the test expectations lines in line_list | |
| 216 to LayoutTest/TestExpectations. Checks the file for the string | |
| 217 '# Tests added from W3C auto import bot' and writes expectation | |
| 218 lines directly under it. If not found, it writes to the end of | |
| 219 the file. | |
| 220 | |
| 221 Args: | |
| 222 host: A Host object. | |
| 223 path: The path to the file LayoutTest/TestExpectations. | |
| 224 line_list: A list of w3c test expectations lines. | |
| 225 | |
| 226 Returns: | |
| 227 Writes to a file on the filesystem called LayoutTests/TestExpectatio ns. | |
| 228 """ | |
| 229 file_contents = host.filesystem.read_text_file(path) | |
| 230 w3c_comment_line_index = file_contents.find('# Tests added from W3C auto import bot') | |
|
qyearsley
2016/07/20 16:55:28
The string '# Tests added from W3C auto import bot
dcampb
2016/07/20 21:11:59
made it into a variable in the function. Don't use
qyearsley
2016/07/20 21:21:45
Sounds fine to me.
Note, sometimes people extract
| |
| 231 all_lines = '' | |
| 232 for line in line_list: | |
| 233 all_lines += str(line) + '\n' | |
| 234 all_lines = all_lines[:-1] | |
| 235 if w3c_comment_line_index == -1: | |
| 236 file_contents += '\n# Tests added from W3C auto import bot\n' | |
| 237 file_contents += all_lines | |
| 238 else: | |
| 239 end_of_comment_line = (file_contents[w3c_comment_line_index:].find(' \n')) + w3c_comment_line_index | |
| 240 new_data = file_contents[: end_of_comment_line + 1] + all_lines + fi le_contents[end_of_comment_line:] | |
| 241 file_contents = new_data | |
| 242 host.filesystem.write_text_file(path, file_contents) | |
| OLD | NEW |