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) | |
Dirk Pranke
2016/08/09 03:59:39
Nit: it's usually a good idea to break up longer f
dcampb
2016/08/09 18:10:44
Interesting, noted.
| |
41 | |
42 | |
43 class W3CExpectationsLineAdder(object): | |
44 | |
45 def __init__(self, host): | |
46 self._host = host | |
47 self.filesystem = host.filesystem | |
48 | |
49 def get_issue_number(self): | |
50 return self._host.scm().get_issue_number() | |
51 | |
52 def get_try_bots(self): | |
53 return self._host.builders.all_try_builder_names() | |
54 | |
55 def _generate_results_dict(self, platform, result_list): | |
56 test_dict = {} | |
57 if '-' in platform: | |
58 platform = platform[platform.find('-') + 1:].capitalize() | |
59 for result in result_list: | |
60 test_dict[result.test_name()] = { | |
61 platform: { | |
62 'expected': result.expected_results(), | |
63 'actual': result.actual_results(), | |
64 'bug': 'crbug.com/626703' | |
65 }} | |
66 return test_dict | |
67 | |
68 def get_failing_results_dict(self, builder, build): | |
69 """Returns a nested dict of failing test results. | |
70 | |
71 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. | |
73 | |
74 Args: | |
75 builder: A Builder object. | |
76 build: A Build object. | |
77 | |
78 Returns: | |
79 A dictionary with the structure: { | |
80 'key': { | |
81 'expected': 'TIMEOUT', | |
82 'actual': 'CRASH', | |
83 'bug': 'crbug.com/11111' | |
84 } | |
85 } | |
86 """ | |
87 layout_test_results = builder.fetch_layout_test_results(build.results_ur l()) | |
88 builder_name = layout_test_results.builder_name() | |
89 platform = self._host.builders.port_name_for_builder_name(builder_name) | |
90 result_list = layout_test_results.didnt_run_as_expected_results() | |
91 failing_results_dict = self._generate_results_dict(platform, result_list ) | |
92 return failing_results_dict | |
93 | |
94 def merge_dicts(self, target, source, path=None): | |
95 """Recursively merge nested dictionaries, returning the target dictionar y | |
96 | |
97 Merges the keys and values from the source dict into the target dict. | |
98 | |
99 Args: | |
100 target: A dictionary where the keys and values from source are updat ed | |
101 in. | |
102 source: A dictionary that provides the new keys and values to be add ed | |
103 into target. | |
104 | |
105 Returns: | |
106 An updated target dictionary. | |
107 """ | |
108 path = path or [] | |
109 for key in source: | |
110 if key in target: | |
111 if (isinstance(target[key], dict)) and isinstance(source[key], d ict): | |
112 self.merge_dicts(target[key], source[key], path + [str(key)] ) | |
113 elif target[key] == source[key]: | |
114 pass | |
115 else: | |
116 raise ValueError('The key: %s already exist in the target di ctionary.' % '.'.join(path)) | |
117 else: | |
118 target[key] = source[key] | |
119 return target | |
120 | |
121 def merge_same_valued_keys(self, dictionary): | |
122 """Merges keys in dictionary with same value. | |
123 | |
124 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 | |
126 are removed from the dict. | |
127 | |
128 Args: | |
129 dictionary: A dictionary with a dictionary as the value. | |
130 | |
131 Returns: | |
132 A dictionary with updated keys to reflect matching values of keys. | |
133 Example: { | |
134 'one': {'foo': 'bar'}, | |
135 'two': {'foo': 'bar'}, | |
136 'three': {'foo': bar'} | |
137 } | |
138 is converted to {('one', 'two', 'three'): {'foo': 'bar'}} | |
139 """ | |
140 matching_value_keys = set() | |
141 keys = dictionary.keys() | |
142 is_last_item = False | |
143 for index, item in enumerate(keys): | |
144 if is_last_item: | |
145 break | |
146 for i in range(index + 1, len(keys)): | |
147 next_item = keys[i] | |
148 if dictionary[item] == dictionary[next_item]: | |
149 matching_value_keys.update([item, next_item]) | |
150 dictionary[tuple(matching_value_keys)] = dictionary[item] | |
151 is_last_item = next_item == keys[-1] | |
152 del dictionary[item] | |
153 del dictionary[next_item] | |
154 return dictionary | |
155 | |
156 def get_expectations(self, results): | |
157 """Returns a list of test expectations for a given test dict. | |
158 | |
159 Returns a list of one or more test expectations based on the expected | |
160 and actual results of a given test name. | |
161 | |
162 Args: | |
163 results: A dictionary that maps one test to its results. Example: { | |
164 'test_name': { | |
165 'expected': 'PASS', | |
166 'actual': 'FAIL', | |
167 'bug': 'crbug.com/11111' | |
168 } | |
169 } | |
170 | |
171 Returns: | |
172 A list of one or more test expectations with the first letter capita lized. Example: | |
173 ['Failure', 'Timeout'] | |
174 """ | |
175 expectations = [] | |
176 failure_expectations = ['TEXT', 'FAIL', 'IMAGE+TEXT', 'IMAGE'] | |
177 pass_crash_timeout = ['TIMEOUT', 'CRASH', 'PASS'] | |
178 if results['expected'] in pass_crash_timeout and results['actual'] in fa ilure_expectations: | |
179 expectations.append('Failure') | |
180 if results['expected'] in failure_expectations and results['actual'] in pass_crash_timeout: | |
181 expectations.append(results['actual'].capitalize()) | |
182 if results['expected'] in pass_crash_timeout and results['actual'] in pa ss_crash_timeout: | |
183 expectations.append(results['actual'].capitalize()) | |
184 expectations.append(results['expected'].capitalize()) | |
185 return expectations | |
186 | |
187 def create_line_list(self, merged_results): | |
188 """Creates list of test expectations lines. | |
189 | |
190 Traverses through a merged_results and parses the value to create a test | |
191 expectations line per key. | |
192 | |
193 Args: | |
194 merged_results: A merged_results with the format { | |
195 'test_name': { | |
196 'platform': { | |
197 'expected: 'PASS', | |
198 'actual': 'FAIL', | |
199 'bug': 'crbug.com/11111' | |
200 } | |
201 } | |
202 } | |
203 It is possible for the dicitonary to have many test_name | |
204 keys. | |
205 | |
206 Returns: | |
207 A list of test expectations lines with the format | |
208 ['BUG_URL [PLATFORM(S)] TEST_MAME [EXPECTATION(S)]'] | |
Dirk Pranke
2016/08/09 03:59:39
Nit: typo ("TEST_MAME").
dcampb
2016/08/09 18:10:44
done
| |
209 """ | |
210 line_list = [] | |
211 for test_name, platform_results in merged_results.iteritems(): | |
212 for platform in platform_results: | |
213 platform_list = [] | |
214 bug = [] | |
215 expectations = [] | |
216 if isinstance(platform, tuple): | |
217 platform_list = list(platform) | |
218 else: | |
219 platform_list.append(platform) | |
220 bug.append(platform_results[platform]['bug']) | |
221 expectations = self.get_expectations(platform_results[platform]) | |
222 line = '%s [ %s ] %s [ %s ]' % (bug[0], ' '.join(platform_list), test_name, ' '.join(expectations)) | |
223 line_list.append(str(line)) | |
Dirk Pranke
2016/08/09 03:59:39
similar comment about blank lines.
| |
224 return line_list | |
225 | |
226 def write_to_test_expectations(self, host, path, line_list): | |
227 """Writes to TestExpectations. | |
228 | |
229 Writes to the test expectations lines in line_list | |
230 to LayoutTest/TestExpectations. Checks the file for the string | |
231 '# Tests added from W3C auto import bot' and writes expectation | |
232 lines directly under it. If not found, it writes to the end of | |
233 the file. | |
234 | |
235 Args: | |
236 host: A Host object. | |
237 path: The path to the file LayoutTest/TestExpectations. | |
238 line_list: A list of w3c test expectations lines. | |
239 | |
240 Returns: | |
241 Writes to a file on the filesystem called LayoutTests/TestExpectatio ns. | |
Dirk Pranke
2016/08/09 03:59:39
This doc string is a bit weird. You take the path
dcampb
2016/08/09 18:10:44
noted
| |
242 """ | |
243 comment_line = '# Tests added from W3C auto import bot' | |
244 file_contents = host.filesystem.read_text_file(path) | |
245 w3c_comment_line_index = file_contents.find(comment_line) | |
246 all_lines = '' | |
247 for line in line_list: | |
248 all_lines += str(line) + '\n' | |
249 all_lines = all_lines[:-1] | |
250 if w3c_comment_line_index == -1: | |
251 file_contents += '\n%s\n' % comment_line | |
252 file_contents += all_lines | |
253 else: | |
254 end_of_comment_line = (file_contents[w3c_comment_line_index:].find(' \n')) + w3c_comment_line_index | |
255 new_data = file_contents[: end_of_comment_line + 1] + all_lines + fi le_contents[end_of_comment_line:] | |
256 file_contents = new_data | |
257 host.filesystem.write_text_file(path, file_contents) | |
Dirk Pranke
2016/08/09 03:59:39
same.
| |
OLD | NEW |