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 """A script to modify TestExpectations lines based layout test failures in try j obs. | 5 """A script to modify TestExpectations lines based layout test failures in try j obs. |
6 | 6 |
7 This script outputs a list of test expectation lines to add to a 'TestExpectatio ns' file | 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. | 8 by retrieving the try job results for the current CL. |
9 """ | 9 """ |
10 | 10 |
11 import logging | 11 import logging |
12 | 12 |
13 from webkitpy.common.net import buildbot | 13 from webkitpy.common.net.buildbot import BuildBot |
14 from webkitpy.common.net import rietveld | 14 from webkitpy.common.net import rietveld |
15 | 15 |
16 | 16 |
17 _log = logging.getLogger(__name__) | 17 _log = logging.getLogger(__name__) |
18 | 18 |
19 | 19 |
20 def main(host, port): | 20 def main(host, port): |
21 expectations_file = port.path_to_generic_test_expectations_file() | 21 expectations_file = port.path_to_generic_test_expectations_file() |
22 expectations_line_adder = W3CExpectationsLineAdder(host) | 22 expectations_line_adder = W3CExpectationsLineAdder(host) |
23 issue_number = expectations_line_adder.get_issue_number() | 23 issue_number = expectations_line_adder.get_issue_number() |
24 try_bots = expectations_line_adder.get_try_bots() | 24 try_bots = expectations_line_adder.get_try_bots() |
25 try_jobs = rietveld.latest_try_jobs(issue_number, try_bots, host.web) | 25 try_jobs = rietveld.latest_try_jobs(issue_number, try_bots, host.web) |
26 line_expectations_dict = {} | 26 test_expectations = {} |
27 if not try_jobs: | 27 if not try_jobs: |
28 print 'No Try Job information was collected.' | 28 print 'No Try Job information was collected.' |
29 return 1 | 29 return 1 |
30 for try_job in try_jobs: | 30 for job in try_jobs: |
31 builder_name = try_job[0] | 31 platform_results = expectations_line_adder.get_failing_results_dict(Buil dBot(), job.builder_name, job.build_number) |
32 build_number = try_job[1] | 32 test_expectations = expectations_line_adder.merge_dicts(test_expectation s, platform_results) |
33 builder = buildbot.Builder(builder_name, host.buildbot) | 33 for test_name, platform_result in test_expectations.iteritems(): |
34 build = buildbot.Build(builder, build_number) | 34 test_expectations[test_name] = expectations_line_adder.merge_same_valued _keys(platform_result) |
35 platform_results_dict = expectations_line_adder.get_failing_results_dict (builder, build) | 35 test_expectation_lines = expectations_line_adder.create_line_list(test_expec tations) |
36 line_expectations_dict = expectations_line_adder.merge_dicts(line_expect ations_dict, platform_results_dict) | 36 expectations_line_adder.write_to_test_expectations(host, expectations_file, test_expectation_lines) |
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 | 37 |
42 | 38 |
43 class W3CExpectationsLineAdder(object): | 39 class W3CExpectationsLineAdder(object): |
44 | 40 |
45 def __init__(self, host): | 41 def __init__(self, host): |
46 self._host = host | 42 self._host = host |
47 self.filesystem = host.filesystem | 43 self.filesystem = host.filesystem |
48 | 44 |
49 def get_issue_number(self): | 45 def get_issue_number(self): |
50 return self._host.scm().get_issue_number() | 46 return self._host.scm().get_issue_number() |
51 | 47 |
52 def get_try_bots(self): | 48 def get_try_bots(self): |
53 return self._host.builders.all_try_builder_names() | 49 return self._host.builders.all_try_builder_names() |
54 | 50 |
55 def _generate_results_dict(self, platform, result_list): | 51 def _generate_results_dict(self, platform, result_list): |
56 test_dict = {} | 52 test_dict = {} |
57 if '-' in platform: | 53 if '-' in platform: |
58 platform = platform[platform.find('-') + 1:].capitalize() | 54 platform = platform[platform.find('-') + 1:].capitalize() |
59 for result in result_list: | 55 for result in result_list: |
60 test_dict[result.test_name()] = { | 56 test_dict[result.test_name()] = { |
61 platform: { | 57 platform: { |
62 'expected': result.expected_results(), | 58 'expected': result.expected_results(), |
63 'actual': result.actual_results(), | 59 'actual': result.actual_results(), |
64 'bug': 'crbug.com/626703' | 60 'bug': 'crbug.com/626703' |
65 }} | 61 }} |
66 return test_dict | 62 return test_dict |
67 | 63 |
68 def get_failing_results_dict(self, builder, build): | 64 def get_failing_results_dict(self, buildbot, builder_name, build_number): |
69 """Returns a nested dict of failing test results. | 65 """Returns a nested dict of failing test results. |
70 | 66 |
71 Retrieves a full list of layout test results from a builder result URL. Collects | 67 Retrieves a full list of layout test results from a builder result URL. Collects |
72 the builder name, platform and a list of tests that did not run as expec ted. | 68 the builder name, platform and a list of tests that did not run as expec ted. |
73 | 69 |
74 Args: | 70 Args: |
75 builder: A Builder object. | 71 builder: A Builder object. |
76 build: A Build object. | 72 build: A Build object. |
77 | 73 |
78 Returns: | 74 Returns: |
79 A dictionary with the structure: { | 75 A dictionary with the structure: { |
80 'key': { | 76 'key': { |
81 'expected': 'TIMEOUT', | 77 'expected': 'TIMEOUT', |
82 'actual': 'CRASH', | 78 'actual': 'CRASH', |
83 'bug': 'crbug.com/11111' | 79 'bug': 'crbug.com/11111' |
84 } | 80 } |
85 } | 81 } |
86 """ | 82 """ |
87 layout_test_results = builder.fetch_layout_test_results(build.results_ur l()) | 83 results_url = buildbot.results_url(builder_name, build_number) |
88 builder_name = layout_test_results.builder_name() | 84 layout_test_results = buildbot.fetch_layout_test_results(results_url) |
89 platform = self._host.builders.port_name_for_builder_name(builder_name) | 85 platform = self._host.builders.port_name_for_builder_name(builder_name) |
90 result_list = layout_test_results.didnt_run_as_expected_results() | 86 result_list = layout_test_results.didnt_run_as_expected_results() |
91 failing_results_dict = self._generate_results_dict(platform, result_list ) | 87 failing_results_dict = self._generate_results_dict(platform, result_list ) |
92 return failing_results_dict | 88 return failing_results_dict |
93 | 89 |
94 def merge_dicts(self, target, source, path=None): | 90 def merge_dicts(self, target, source, path=None): |
95 """Recursively merge nested dictionaries, returning the target dictionar y | 91 """Recursively merge nested dictionaries, returning the target dictionar y |
96 | 92 |
97 Merges the keys and values from the source dict into the target dict. | 93 Merges the keys and values from the source dict into the target dict. |
98 | 94 |
(...skipping 23 matching lines...) Expand all Loading... | |
122 """Merges keys in dictionary with same value. | 118 """Merges keys in dictionary with same value. |
123 | 119 |
124 Traverses through a dict and compares the values of keys to one another. | 120 Traverses through a dict and compares the values of keys to one another. |
125 If the values match, the keys are combined to a tuple and the previous k eys | 121 If the values match, the keys are combined to a tuple and the previous k eys |
126 are removed from the dict. | 122 are removed from the dict. |
127 | 123 |
128 Args: | 124 Args: |
129 dictionary: A dictionary with a dictionary as the value. | 125 dictionary: A dictionary with a dictionary as the value. |
130 | 126 |
131 Returns: | 127 Returns: |
132 A dictionary with updated keys to reflect matching values of keys. | 128 A new dictionary with updated keys to reflect matching values of key s. |
133 Example: { | 129 Example: { |
134 'one': {'foo': 'bar'}, | 130 'one': {'foo': 'bar'}, |
135 'two': {'foo': 'bar'}, | 131 'two': {'foo': 'bar'}, |
136 'three': {'foo': bar'} | 132 'three': {'foo': 'bar'} |
137 } | 133 } |
138 is converted to {('one', 'two', 'three'): {'foo': 'bar'}} | 134 is converted to a new dictionary with that contains |
135 {('one', 'two', 'three'): {'foo': 'bar'}} | |
139 """ | 136 """ |
137 merged_dict = {} | |
140 matching_value_keys = set() | 138 matching_value_keys = set() |
141 keys = dictionary.keys() | 139 keys = sorted(dictionary.keys()) |
142 is_last_item = False | 140 while keys: |
143 for index, item in enumerate(keys): | 141 current_key = keys[0] |
144 if is_last_item: | 142 found_match = False |
143 if current_key == keys[-1]: | |
144 merged_dict[current_key] = dictionary[current_key] | |
145 keys.remove(current_key) | |
145 break | 146 break |
146 for i in range(index + 1, len(keys)): | 147 for next_item in keys[1:]: |
147 next_item = keys[i] | 148 if dictionary[current_key] == dictionary[next_item]: |
148 if dictionary[item] == dictionary[next_item]: | 149 found_match = True |
149 matching_value_keys.update([item, next_item]) | 150 matching_value_keys.update([current_key, next_item]) |
150 dictionary[tuple(matching_value_keys)] = dictionary[item] | 151 if next_item == keys[-1]: |
151 is_last_item = next_item == keys[-1] | 152 if found_match: |
152 del dictionary[item] | 153 merged_dict[tuple(matching_value_keys)] = dictionary[cur rent_key] |
153 del dictionary[next_item] | 154 keys = [k for k in keys if k not in matching_value_keys] |
154 return dictionary | 155 else: |
156 merged_dict[current_key] = dictionary[current_key] | |
157 keys.remove(current_key) | |
158 matching_value_keys = set() | |
159 return merged_dict | |
155 | 160 |
156 def get_expectations(self, results): | 161 def get_expectations(self, results): |
157 """Returns a list of test expectations for a given test dict. | 162 """Returns a list of test expectations for a given test dict. |
qyearsley
2016/07/26 23:59:43
In particular, it's a list of a expectations which
dcampb
2016/07/27 16:26:36
This function returns a list of actual test expect
qyearsley
2016/07/27 17:25:39
Right, so the output of this function determines t
dcampb
2016/07/27 18:17:50
agreed.
| |
158 | 163 |
159 Returns a list of one or more test expectations based on the expected | 164 Returns a list of one or more test expectations based on the expected |
160 and actual results of a given test name. | 165 and actual results of a given test name. |
qyearsley
2016/07/27 17:25:39
This function has a precondition or assumption tha
| |
161 | 166 |
162 Args: | 167 Args: |
163 results: A dictionary that maps one test to its results. Example: { | 168 results: A dictionary that maps one test to its results. Example: { |
164 'test_name': { | 169 'test_name': { |
165 'expected': 'PASS', | 170 'expected': 'PASS', |
166 'actual': 'FAIL', | 171 'actual': 'FAIL', |
167 'bug': 'crbug.com/11111' | 172 'bug': 'crbug.com/11111' |
168 } | 173 } |
169 } | 174 } |
170 | 175 |
171 Returns: | 176 Returns: |
172 A list of one or more test expectations with the first letter capita lized. Example: | 177 A list of one or more test expectations with the first letter capita lized. Example: |
173 ['Failure', 'Timeout'] | 178 ['Failure', 'Timeout'] |
qyearsley
2016/07/26 23:59:43
This comment can be updated now that this function
dcampb
2016/07/27 18:17:49
done
| |
174 """ | 179 """ |
175 expectations = [] | 180 expectations = set() |
176 failure_expectations = ['TEXT', 'FAIL', 'IMAGE+TEXT', 'IMAGE'] | 181 failure_expectations = ['SLOW', 'TEXT', 'FAIL', 'IMAGE+TEXT', 'IMAGE'] |
qyearsley
2016/07/26 23:59:43
I'm not sure if SLOW counts as a failure, and also
dcampb
2016/07/27 16:26:36
I do remember seeing an example of an expected tha
qyearsley
2016/07/27 17:25:39
Alright; here shouldn't be any more "baseline mism
| |
177 pass_crash_timeout = ['TIMEOUT', 'CRASH', 'PASS'] | 182 pass_crash_timeout = ['TIMEOUT', 'CRASH', 'PASS'] |
178 if results['expected'] in pass_crash_timeout and results['actual'] in fa ilure_expectations: | 183 for expected in results['expected'].split(): |
179 expectations.append('Failure') | 184 for actual in results['actual'].split(): |
qyearsley
2016/07/26 23:59:43
This morning I was saying that if there are multip
dcampb
2016/07/27 16:26:36
Would that result in a mismatch result? or wouldn'
dcampb
2016/07/27 16:44:24
Correction: This script only works with tests that
qyearsley
2016/07/27 17:25:39
What I meant was, since the dict passed in contain
dcampb
2016/07/27 18:17:49
I see what your saying. I agree, it could be refac
| |
180 if results['expected'] in failure_expectations and results['actual'] in pass_crash_timeout: | 185 if expected in pass_crash_timeout and actual in failure_expectat ions: |
181 expectations.append(results['actual'].capitalize()) | 186 expectations.add('Failure') |
182 if results['expected'] in pass_crash_timeout and results['actual'] in pa ss_crash_timeout: | 187 if expected in failure_expectations and actual in pass_crash_tim eout: |
183 expectations.append(results['actual'].capitalize()) | 188 expectations.add(actual.capitalize()) |
184 expectations.append(results['expected'].capitalize()) | 189 if expected in pass_crash_timeout and actual in pass_crash_timeo ut: |
190 expectations.add(actual.capitalize()) | |
qyearsley
2016/07/26 23:59:43
What if expected is "PASS" and actual is "PASS" --
dcampb
2016/07/27 16:26:36
That wouldn't happen as this script only parses te
dcampb
2016/07/27 16:44:24
Correction: This script parses tests that did not
qyearsley
2016/07/27 17:25:39
Yeah, those names aren't super clear. "Mismatch" w
dcampb
2016/07/27 18:17:50
understood. I think it would be wise to get this c
| |
185 return expectations | 191 return expectations |
186 | 192 |
187 def create_line_list(self, merged_results): | 193 def create_line_list(self, merged_results): |
188 """Creates list of test expectations lines. | 194 """Creates list of test expectations lines. |
189 | 195 |
190 Traverses through a merged_results and parses the value to create a test | 196 Traverses through a merged_results and parses the value to create a test |
191 expectations line per key. | 197 expectations line per key. |
192 | 198 |
193 Args: | 199 Args: |
194 merged_results: A merged_results with the format { | 200 merged_results: A merged_results with the format { |
195 'test_name': { | 201 'test_name': { |
196 'platform': { | 202 'platform': { |
197 'expected: 'PASS', | 203 'expected: 'PASS', |
198 'actual': 'FAIL', | 204 'actual': 'FAIL', |
199 'bug': 'crbug.com/11111' | 205 'bug': 'crbug.com/11111' |
200 } | 206 } |
201 } | 207 } |
202 } | 208 } |
203 It is possible for the dicitonary to have many test_name | 209 It is possible for the dicitonary to have many test_name |
204 keys. | 210 keys. |
205 | 211 |
206 Returns: | 212 Returns: |
207 A list of test expectations lines with the format | 213 A list of test expectations lines with the format |
208 ['BUG_URL [PLATFORM(S)] TEST_MAME [EXPECTATION(S)]'] | 214 ['BUG_URL [PLATFORM(S)] TEST_MAME [EXPECTATION(S)]'] |
209 """ | 215 """ |
210 line_list = [] | 216 line_list = [] |
211 for test_name, platform_results in merged_results.iteritems(): | 217 for test_name, platform_results in merged_results.iteritems(): |
212 for platform in platform_results: | 218 for platform in platform_results: |
213 platform_list = [] | 219 if test_name.startswith('imported'): |
214 bug = [] | 220 print platform_results |
215 expectations = [] | 221 platform_list = [] |
216 if isinstance(platform, tuple): | 222 bug = [] |
217 platform_list = list(platform) | 223 expectations = [] |
218 else: | 224 if isinstance(platform, tuple): |
219 platform_list.append(platform) | 225 platform_list = list(platform) |
220 bug.append(platform_results[platform]['bug']) | 226 else: |
221 expectations = self.get_expectations(platform_results[platform]) | 227 platform_list.append(platform) |
222 line = '%s [ %s ] %s [ %s ]' % (bug[0], ' '.join(platform_list), test_name, ' '.join(expectations)) | 228 bug.append(platform_results[platform]['bug']) |
223 line_list.append(str(line)) | 229 expectations = self.get_expectations(platform_results[platfo rm]) |
230 line = '%s [ %s ] %s [ %s ]' % (bug[0], ' '.join(platform_li st), test_name, ' '.join(expectations)) | |
231 line_list.append(str(line)) | |
224 return line_list | 232 return line_list |
225 | 233 |
226 def write_to_test_expectations(self, host, path, line_list): | 234 def write_to_test_expectations(self, host, path, line_list): |
227 """Writes to TestExpectations. | 235 """Writes to TestExpectations. |
228 | 236 |
229 Writes to the test expectations lines in line_list | 237 Writes to the test expectations lines in line_list |
230 to LayoutTest/TestExpectations. Checks the file for the string | 238 to LayoutTest/TestExpectations. Checks the file for the string |
231 '# Tests added from W3C auto import bot' and writes expectation | 239 '# Tests added from W3C auto import bot' and writes expectation |
232 lines directly under it. If not found, it writes to the end of | 240 lines directly under it. If not found, it writes to the end of |
233 the file. If the test name is already in LayoutTests/TestExpectations, | 241 the file. If the test name is already in LayoutTests/TestExpectations, |
(...skipping 19 matching lines...) Expand all Loading... | |
253 all_lines += str(line) + '\n' | 261 all_lines += str(line) + '\n' |
254 all_lines = all_lines[:-1] | 262 all_lines = all_lines[:-1] |
255 if w3c_comment_line_index == -1: | 263 if w3c_comment_line_index == -1: |
256 file_contents += '\n%s\n' % comment_line | 264 file_contents += '\n%s\n' % comment_line |
257 file_contents += all_lines | 265 file_contents += all_lines |
258 else: | 266 else: |
259 end_of_comment_line = (file_contents[w3c_comment_line_index:].find(' \n')) + w3c_comment_line_index | 267 end_of_comment_line = (file_contents[w3c_comment_line_index:].find(' \n')) + w3c_comment_line_index |
260 new_data = file_contents[: end_of_comment_line + 1] + all_lines + fi le_contents[end_of_comment_line:] | 268 new_data = file_contents[: end_of_comment_line + 1] + all_lines + fi le_contents[end_of_comment_line:] |
261 file_contents = new_data | 269 file_contents = new_data |
262 host.filesystem.write_text_file(path, file_contents) | 270 host.filesystem.write_text_file(path, file_contents) |
OLD | NEW |