forked from chromium/chromium
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathupdate_expectations.py
345 lines (289 loc) · 13.4 KB
/
update_expectations.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Updates TestExpectations based on results in builder bots.
Scans the TestExpectations file and uses results from actual builder bots runs
to remove tests that are marked as flaky or failing but don't fail in the
specified way.
--type=flake updates only lines that incude a 'Pass' expectation plus at
least one other expectation.
--type=fail updates lines that include only 'Failure', 'Timeout', or
'Crash' expectations.
E.g. If a test has this expectation:
bug(test) fast/test.html [ Failure Pass ]
And all the runs on builders have passed the line will be removed.
Additionally, the runs don't all have to be Passing to remove the line;
as long as the non-Passing results are of a type not specified in the
expectation this line will be removed. For example, if this is the
expectation:
bug(test) fast/test.html [ Crash Pass ]
But the results on the builders show only Passes and Timeouts, the line
will be removed since there's no Crash results.
"""
import argparse
import logging
import webbrowser
from blinkpy.tool.commands.flaky_tests import FlakyTests
from blinkpy.web_tests.models.test_expectations import TestExpectations, SPECIAL_PREFIXES
from blinkpy.web_tests.models.typ_types import ResultType
_log = logging.getLogger(__name__)
CHROMIUM_BUG_PREFIX = 'crbug.com/'
def main(host, bot_test_expectations_factory, argv):
parser = argparse.ArgumentParser(
epilog=__doc__, formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument(
'--type',
choices=['flake', 'fail', 'all'],
default='all',
help='type of expectations to update (default: %(default)s)')
parser.add_argument(
'--verbose',
'-v',
action='store_true',
default=False,
help='enable more verbose logging')
parser.add_argument(
'--remove-missing',
action='store_true',
default=False,
help='also remove lines if there were no results, '
'e.g. Android-only expectations for tests '
'that are not in SmokeTests')
# TODO(crbug.com/1077883): Including cq results might introduce false
# negatives. An in-review change that caused a test with fail test
# expectation but no longer fails, to fail again, will result in
# the test expectation not being removed even if the change
# does not ultimately land.
parser.add_argument(
'--include-cq-results',
action='store_true',
default=False,
help='include results from cq.')
parser.add_argument(
'--show-results',
'-s',
action='store_true',
default=False,
help='Open results dashboard for all removed lines')
args = parser.parse_args(argv)
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO,
format='%(levelname)s: %(message)s')
port = host.port_factory.get()
expectations_file = port.path_to_generic_test_expectations_file()
if not host.filesystem.isfile(expectations_file):
_log.warning("Didn't find generic expectations file at: " +
expectations_file)
return 1
remover = ExpectationsRemover(host, port, bot_test_expectations_factory,
webbrowser, args.type, args.remove_missing,
args.include_cq_results)
test_expectations = remover.get_updated_test_expectations()
if args.show_results:
remover.show_removed_results()
host.filesystem.write_text_file(expectations_file, test_expectations)
remover.print_suggested_commit_description()
return 0
class ExpectationsRemover(object):
def __init__(self,
host,
port,
bot_test_expectations_factory,
browser,
type_flag='all',
remove_missing=False,
include_cq_results=False):
self._host = host
self._port = port
self._expectations_factory = bot_test_expectations_factory
self._browser = browser
self._expectations_to_remove_list = None
self._type = type_flag
self._bug_numbers = set()
self._remove_missing = remove_missing
self._include_cq_results = include_cq_results
self._builder_results_by_path = self._get_builder_results_by_path()
self._removed_test_names = set()
self._version_to_os = {}
def _can_delete_line(self, expectation):
"""Returns whether a given line in the expectations can be removed.
Uses results from builder bots to determine if a given line is stale and
can safely be removed from the TestExpectations file. (i.e. remove if
the bots show that it's not flaky.) There are also some rules about when
not to remove lines (e.g. never remove lines with Rebaseline
expectations, don't remove non-flaky expectations, etc.)
Args:
test_expectation_line (TestExpectationLine): A line in the test
expectation file to test for possible removal.
Returns:
True if the line can be removed, False otherwise.
"""
expected_results = expectation.results
if not expected_results:
return False
# Don't check lines that have expectations like Skip or Slow.
if ResultType.Skip in expected_results or expectation.is_slow_test:
return False
# Don't check consistent passes.
if len(expected_results) == 1 and ResultType.Pass in expected_results:
return False
# Don't check flakes in fail mode.
if self._type == 'fail' and ResultType.Pass in expected_results:
return False
# Don't check failures in flake mode.
if self._type == 'flake' and ResultType.Pass not in expected_results:
return False
# Initialize OS version to OS dictionary.
if not self._version_to_os:
for os, os_versions in \
self._port.configuration_specifier_macros().items():
for version in os_versions:
self._version_to_os[version.lower()] = os.lower()
# The line can be deleted if none of the expectations appear in the
# actual results or only a PASS expectation appears in the actual
# results.
builders_checked = []
builders = []
for config in self._port.all_test_configurations():
if set(expectation.tags).issubset(
set([
self._version_to_os[config.version.lower()],
config.version.lower(),
config.build_type.lower()
])):
try_server_configs = [False]
if self._include_cq_results:
try_server_configs.append(True)
for is_try_server in try_server_configs:
builder_name = self._host.builders.builder_name_for_specifiers(
config.version, config.build_type, is_try_server)
if not builder_name:
_log.debug('No builder with config %s for %s',
config, 'CQ' if is_try_server else 'CI')
# For many configurations, there is no matching builder in
# blinkpy/common/config/builders.json. We ignore these
# configurations and make decisions based only on configurations
# with actual builders.
continue
builders.append(builder_name)
for builder_name in builders:
builders_checked.append(builder_name)
if builder_name not in self._builder_results_by_path.keys():
_log.error('Failed to find results for builder "%s"',
builder_name)
return False
results_by_path = self._builder_results_by_path[builder_name]
# No results means the tests were all skipped, or all results are passing.
if expectation.test not in results_by_path.keys():
if self._remove_missing:
continue
return False
results_for_single_test = set(results_by_path[expectation.test])
expectations_met = expected_results & results_for_single_test
if (expectations_met != set([ResultType.Pass])
and expectations_met != set([])):
return False
if builders_checked:
_log.debug('Checked builders:\n %s',
'\n '.join(builders_checked))
else:
_log.warning('No matching builders for line, deleting line.')
return True
def _get_builder_results_by_path(self):
"""Returns a dictionary of results for each builder.
Returns a dictionary where each key is a builder and value is a dictionary containing
the distinct results for each test. E.g.
{
'WebKit Linux Precise': {
'test1.html': ['PASS', 'IMAGE'],
'test2.html': ['PASS'],
},
'WebKit Mac10.10': {
'test1.html': ['PASS', 'IMAGE'],
'test2.html': ['PASS', 'TEXT'],
}
}
"""
builder_results_by_path = {}
builders = []
if self._include_cq_results:
builders = self._host.builders.all_builder_names()
else:
builders = self._host.builders.all_continuous_builder_names()
for builder_name in builders:
expectations_for_builder = (
self._expectations_factory.expectations_for_builder(builder_name)
)
if not expectations_for_builder:
# This is not fatal since we may not need to check these
# results. If we do need these results we'll log an error later
# when trying to check against them.
_log.warning(
'Downloaded results are missing results for builder "%s"',
builder_name)
continue
builder_results_by_path[builder_name] = (
expectations_for_builder.all_results_by_path())
return builder_results_by_path
def get_updated_test_expectations(self):
"""Filters out passing lines from TestExpectations file.
Reads the current TestExpectations file and, using results from the
build bots, removes lines that are passing. That is, removes lines that
were not needed to keep the bots green.
Returns:
A TestExpectations object with the passing lines filtered out.
"""
generic_exp_path = self._port.path_to_generic_test_expectations_file()
raw_test_expectations = self._host.filesystem.read_text_file(
generic_exp_path)
expectations_dict = {generic_exp_path: raw_test_expectations}
test_expectations = TestExpectations(
port=self._port, expectations_dict=expectations_dict)
removed_exps = []
lines = []
for exp in test_expectations.get_updated_lines(generic_exp_path):
# only get expectations objects for non glob patterns
if not exp.test or exp.is_glob:
continue
if self._can_delete_line(exp):
reason = exp.reason or ''
self._bug_numbers.update([
reason[len(CHROMIUM_BUG_PREFIX):]
for reason in reason.split()
if reason.startswith(CHROMIUM_BUG_PREFIX)
])
self._removed_test_names.add(exp.test)
removed_exps.append(exp)
_log.info('Deleting line "%s"' % exp.to_string().strip())
if removed_exps:
test_expectations.remove_expectations(generic_exp_path,
removed_exps)
return '\n'.join([
e.to_string()
for e in test_expectations.get_updated_lines(generic_exp_path)
])
def show_removed_results(self):
"""Opens a browser showing the removed lines in the results dashboard.
Opens the results dashboard in the browser, showing all the tests for
lines removed from the TestExpectations file, allowing the user to
manually confirm the results.
"""
url = self._flakiness_dashboard_url()
_log.info('Opening results dashboard: ' + url)
self._browser.open(url)
def print_suggested_commit_description(self):
"""Prints the body of a suggested CL description after removing some lines."""
expectation_type = ''
if self._type != 'all':
expectation_type = self._type + ' '
dashboard_url = self._flakiness_dashboard_url()
bugs = ', '.join(sorted(self._bug_numbers))
message = (
'Remove %sTestExpectations which are not failing in the specified way.\n\n'
'This change was made by the update_expectations.py script.\n\n'
'Recent test results history:\n%s\n\n'
'Bug: %s') % (expectation_type, dashboard_url, bugs)
_log.info('Suggested commit description:\n' + message)
def _flakiness_dashboard_url(self):
removed_test_names = ','.join(sorted(self._removed_test_names))
return FlakyTests.FLAKINESS_DASHBOARD_URL % removed_test_names