blob: 1d7e0312b2b791d0626ac30a40c97ed3a9b00756 [file] [log] [blame]
Dan Shi7e04fa82013-07-25 15:08:48 -07001#!/usr/bin/python
2#
3# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Tool to validate code in prod branch before pushing to lab.
8
9The script runs push_to_prod suite to verify code in prod branch is ready to be
10pushed. Link to design document:
11https://docs.google.com/a/google.com/document/d/1JMz0xS3fZRSHMpFkkKAL_rxsdbNZomhHbC3B8L71uuI/edit
12
13To verify if prod branch can be pushed to lab, run following command in
14chromeos-autotest.cbf server:
Michael Liang52d9f1f2014-06-17 15:01:24 -070015/usr/local/autotest/site_utils/test_push.py -e someone@company.com
Dan Shi7e04fa82013-07-25 15:08:48 -070016
17The script uses latest stumpy canary build as test build by default.
18
19"""
20
21import argparse
22import getpass
23import os
24import re
25import subprocess
26import sys
27import urllib2
28
29import common
Dan Shia8da7602014-05-09 15:18:15 -070030try:
31 from autotest_lib.frontend import setup_django_environment
32 from autotest_lib.frontend.afe import models
33except ImportError:
34 # Unittest may not have Django database configured and will fail to import.
35 pass
Dan Shi7e04fa82013-07-25 15:08:48 -070036from autotest_lib.client.common_lib import global_config, mail
37from autotest_lib.server import site_utils
38from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
39from autotest_lib.server.cros.dynamic_suite import reporting
Dan Shia8da7602014-05-09 15:18:15 -070040from autotest_lib.server.hosts import cros_host
Dan Shi7e04fa82013-07-25 15:08:48 -070041
42CONFIG = global_config.global_config
43
44MAIL_FROM = 'chromeos-test@google.com'
Dan Shi5ba5d2e2014-05-09 13:47:00 -070045DEVSERVERS = CONFIG.get_config_value('CROS', 'dev_server', type=list,
46 default=[])
47BUILD_REGEX = '^R[\d]+-[\d]+\.[\d]+\.[\d]+$'
Dan Shi7e04fa82013-07-25 15:08:48 -070048RUN_SUITE_COMMAND = 'run_suite.py'
49PUSH_TO_PROD_SUITE = 'push_to_prod'
50AU_SUITE = 'paygen_au_canary'
51
Fang Deng6dddf602014-04-17 17:01:47 -070052SUITE_JOB_START_INFO_REGEX = ('^.*Created suite job:.*'
53 'tab_id=view_job&object_id=(\d+)$')
Dan Shi7e04fa82013-07-25 15:08:48 -070054
55# Dictionary of test results keyed by test name regular expression.
56EXPECTED_TEST_RESULTS = {'^SERVER_JOB$': 'GOOD',
57 # This is related to dummy_Fail/control.dependency.
58 'dummy_Fail.dependency$': 'TEST_NA',
Dan Shi38bd81f2014-05-02 15:07:11 -070059 'telemetry_CrosTests.*': 'GOOD',
Dan Shi7e04fa82013-07-25 15:08:48 -070060 'platform_InstallTestImage_SERVER_JOB$': 'GOOD',
61 'dummy_Pass.*': 'GOOD',
62 'dummy_Fail.Fail$': 'FAIL',
63 'dummy_Fail.RetryFail$': 'FAIL',
64 'dummy_Fail.RetrySuccess': 'GOOD',
65 'dummy_Fail.Error$': 'ERROR',
66 'dummy_Fail.Warn$': 'WARN',
67 'dummy_Fail.NAError$': 'TEST_NA',
68 'dummy_Fail.Crash$': 'GOOD',
69 }
70
71EXPECTED_TEST_RESULTS_AU = {'SERVER_JOB$': 'GOOD',
72 'autoupdate_EndToEndTest.paygen_au_canary_test_delta.*': 'GOOD',
73 'autoupdate_EndToEndTest.paygen_au_canary_test_full.*': 'GOOD',
74 }
75
76# Anchor for the auto-filed bug for dummy_Fail tests.
77BUG_ANCHOR = 'TestFailure(push_to_prod,dummy_Fail.Fail,always fail)'
78
79URL_HOST = CONFIG.get_config_value('SERVER', 'hostname', type=str)
80URL_PATTERN = CONFIG.get_config_value('CROS', 'log_url_pattern', type=str)
81
82# Save all run_suite command output.
83run_suite_output = []
84
85class TestPushException(Exception):
86 """Exception to be raised when the test to push to prod failed."""
87 pass
88
Dan Shi5ba5d2e2014-05-09 13:47:00 -070089
90def get_default_build(devserver=None, board='stumpy'):
91 """Get the default build to be used for test.
92
93 @param devserver: devserver used to look for latest staged build. If value
94 is None, all devservers in config will be tried.
95 @param board: Name of board to be tested, default is stumpy.
96 @return: Build to be tested, e.g., stumpy-release/R36-5881.0.0
97 """
98 LATEST_BUILD_URL_PATTERN = '%s/latestbuild?target=%s-release'
99 build = None
100 if not devserver:
101 for server in DEVSERVERS:
102 url = LATEST_BUILD_URL_PATTERN % (server, board)
103 build = urllib2.urlopen(url).read()
104 if build and re.match(BUILD_REGEX, build):
105 return '%s-release/%s' % (board, build)
106
107 # If no devserver has any build staged for the given board, use the stable
108 # build in config.
109 build = CONFIG.get_config_value('CROS', 'stable_cros_version')
110 return '%s-release/%s' % (board, build)
111
112
Dan Shi7e04fa82013-07-25 15:08:48 -0700113def parse_arguments():
114 """Parse arguments for test_push tool.
115
116 @return: Parsed arguments.
117
118 """
119 parser = argparse.ArgumentParser()
120 parser.add_argument('-b', '--board', dest='board', default='stumpy',
121 help='Default is stumpy.')
122 parser.add_argument('-i', '--build', dest='build', default=None,
123 help='Default is the latest canary build of given '
124 'board. Must be a canary build, otherwise AU test '
125 'will fail.')
126 parser.add_argument('-p', '--pool', dest='pool', default='bvt')
127 parser.add_argument('-u', '--num', dest='num', type=int, default=3,
128 help='Run on at most NUM machines.')
129 parser.add_argument('-f', '--file_bugs', dest='file_bugs', default='True',
130 help='File bugs on test failures. Must pass "True" or '
131 '"False" if used.')
132 parser.add_argument('-e', '--email', dest='email', default=None,
133 help='Email address for the notification to be sent to '
134 'after the script finished running.')
135 parser.add_argument('-d', '--devserver', dest='devserver',
Dan Shi5ba5d2e2014-05-09 13:47:00 -0700136 default=None,
Dan Shi7e04fa82013-07-25 15:08:48 -0700137 help='devserver to find what\'s the latest build.')
138
139 arguments = parser.parse_args(sys.argv[1:])
140
141 # Get latest canary build as default build.
142 if not arguments.build:
Dan Shi5ba5d2e2014-05-09 13:47:00 -0700143 arguments.build = get_default_build(arguments.devserver,
144 arguments.board)
Dan Shi7e04fa82013-07-25 15:08:48 -0700145
146 return arguments
147
148
149def do_run_suite(suite_name, arguments):
150 """Call run_suite to run a suite job, and return the suite job id.
151
152 The script waits the suite job to finish before returning the suite job id.
153 Also it will echo the run_suite output to stdout.
154
155 @param suite_name: Name of a suite, e.g., dummy.
156 @param arguments: Arguments for run_suite command.
157 @return: Suite job ID.
158
159 """
160 dir = os.path.dirname(os.path.realpath(__file__))
161 cmd = [os.path.join(dir, RUN_SUITE_COMMAND),
162 '-s', suite_name,
163 '-b', arguments.board,
164 '-i', arguments.build,
165 '-p', arguments.pool,
166 '-u', str(arguments.num),
167 '-f', arguments.file_bugs]
168
169 suite_job_id = None
Dan Shi7e04fa82013-07-25 15:08:48 -0700170
171 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
172 stderr=subprocess.STDOUT)
173
174 while True:
175 line = proc.stdout.readline()
176
177 # Break when run_suite process completed.
178 if not line and proc.poll() != None:
179 break
180 print line.rstrip()
181 run_suite_output.append(line.rstrip())
182
183 if not suite_job_id:
184 m = re.match(SUITE_JOB_START_INFO_REGEX, line)
185 if m and m.group(1):
186 suite_job_id = int(m.group(1))
187
188 if not suite_job_id:
189 raise TestPushException('Failed to retrieve suite job ID.')
Dan Shia8da7602014-05-09 15:18:15 -0700190
191 print 'Suite job %s is completed.' % suite_job_id
Dan Shi7e04fa82013-07-25 15:08:48 -0700192 return suite_job_id
193
194
Dan Shia8da7602014-05-09 15:18:15 -0700195def check_dut_image(build, suite_job_id):
196 """Confirm all DUTs used for the suite are imaged to expected build.
197
198 @param build: Expected build to be imaged.
199 @param suite_job_id: job ID of the suite job.
200 @raise TestPushException: If a DUT does not have expected build imaged.
201 """
202 print 'Checking image installed in DUTs...'
203 job_ids = [job.id for job in
204 models.Job.objects.filter(parent_job_id=suite_job_id)]
205 hqes = [models.HostQueueEntry.objects.filter(job_id=job_id)[0]
206 for job_id in job_ids]
207 hostnames = set([hqe.host.hostname for hqe in hqes])
208 for hostname in hostnames:
209 host = cros_host.CrosHost(hostname)
210 found_build = host.get_build()
211 if found_build != build:
212 raise TestPushException('DUT is not imaged properly. Host %s has '
213 'build %s, while build %s is expected.' %
214 (hostname, found_build, build))
215
216
Dan Shi7e04fa82013-07-25 15:08:48 -0700217def test_suite(suite_name, expected_results, arguments):
218 """Call run_suite to start a suite job and verify results.
219
220 @param suite_name: Name of a suite, e.g., dummy
221 @param expected_results: A dictionary of test name to test result.
222 @param arguments: Arguments for run_suite command.
223
224 """
225 suite_job_id = do_run_suite(suite_name, arguments)
226
Dan Shia8da7602014-05-09 15:18:15 -0700227 # Confirm all DUTs used for the suite are imaged to expected build.
228 if suite_name != AU_SUITE:
229 check_dut_image(arguments.build, suite_job_id)
230
Dan Shi7e04fa82013-07-25 15:08:48 -0700231 # Find all tests and their status
Dan Shia8da7602014-05-09 15:18:15 -0700232 print 'Comparing test results...'
Dan Shi7e04fa82013-07-25 15:08:48 -0700233 TKO = frontend_wrappers.RetryingTKO(timeout_min=0.1, delay_sec=10)
234 test_views = site_utils.get_test_views_from_tko(suite_job_id, TKO)
235
236 mismatch_errors = []
237 extra_test_errors = []
238
239 found_keys = set()
240 for test_name,test_status in test_views.items():
241 print "%s%s" % (test_name.ljust(30), test_status)
242 test_found = False
243 for key,val in expected_results.items():
244 if re.search(key, test_name):
245 test_found = True
246 found_keys.add(key)
247 # TODO(dshi): result for this test is ignored until servo is
248 # added to a host accessible by cbf server (crbug.com/277109).
249 if key == 'platform_InstallTestImage_SERVER_JOB$':
250 continue
251 # TODO(dshi): result for this test is ignored until the bug is
Dan Shi38bd81f2014-05-02 15:07:11 -0700252 # fixed in Telemetry (crbug.com/369671).
253 if key == 'telemetry_CrosTests.*':
Dan Shi7e04fa82013-07-25 15:08:48 -0700254 continue
255 if val != test_status:
256 error = ('%s Expected: [%s], Actual: [%s]' %
257 (test_name, val, test_status))
258 mismatch_errors.append(error)
259 if not test_found:
260 extra_test_errors.append(test_name)
261
262 missing_test_errors = set(expected_results.keys()) - found_keys
263 # For latest build, npo_test_delta does not exist.
264 if missing_test_errors == set(['autoupdate_EndToEndTest.npo_test_delta.*']):
265 missing_test_errors = set([])
266 # For trybot build, nmo_test_delta does not exist.
267 if missing_test_errors == set(['autoupdate_EndToEndTest.nmo_test_delta.*']):
268 missing_test_errors = set([])
269 summary = []
270 if mismatch_errors:
271 summary.append(('Results of %d test(s) do not match expected '
272 'values:') % len(mismatch_errors))
273 summary.extend(mismatch_errors)
274 summary.append('\n')
275
276 if extra_test_errors:
277 summary.append('%d test(s) are not expected to be run:' %
278 len(extra_test_errors))
279 summary.extend(extra_test_errors)
280 summary.append('\n')
281
282 if missing_test_errors:
283 summary.append('%d test(s) are missing from the results:' %
284 len(missing_test_errors))
285 summary.extend(missing_test_errors)
286 summary.append('\n')
287
288 # Test link to log can be loaded.
289 job_name = '%s-%s' % (suite_job_id, getpass.getuser())
290 log_link = URL_PATTERN % (URL_HOST, job_name)
291 try:
292 urllib2.urlopen(log_link).read()
293 except urllib2.URLError:
294 summary.append('Failed to load page for link to log: %s.' % log_link)
295
296 if summary:
297 raise TestPushException('\n'.join(summary))
298
299
300def close_bug():
301 """Close all existing bugs filed for dummy_Fail.
302
303 @return: A list of issue ids to be used in check_bug_filed_and_deduped.
304 """
305 old_issue_ids = []
306 reporter = reporting.Reporter()
307 while True:
308 issue = reporter.find_issue_by_marker(BUG_ANCHOR)
309 if not issue:
310 return old_issue_ids
311 if issue.id in old_issue_ids:
312 raise TestPushException('Failed to close issue %d' % issue.id)
313 old_issue_ids.append(issue.id)
314 reporter.modify_bug_report(issue.id,
315 comment='Issue closed by test_push script.',
316 label_update='',
317 status='WontFix')
318
319
320def check_bug_filed_and_deduped(old_issue_ids):
321 """Confirm bug related to dummy_Fail was filed and deduped.
322
323 @param old_issue_ids: A list of issue ids that was closed earlier. id of the
324 new issue must be not in this list.
325 @raise TestPushException: If auto bug file failed to create a new issue or
326 dedupe multiple failures.
327 """
328 reporter = reporting.Reporter()
329 issue = reporter.find_issue_by_marker(BUG_ANCHOR)
330 if not issue:
331 raise TestPushException('Auto bug file failed. Unable to locate bug '
332 'with marker %s' % BUG_ANCHOR)
333 if old_issue_ids and issue.id in old_issue_ids:
334 raise TestPushException('Auto bug file failed to create a new issue. '
335 'id of the old issue found is %d.' % issue.id)
336 if not ('%s2' % reporter.AUTOFILED_COUNT) in issue.labels:
337 raise TestPushException(('Auto bug file failed to dedupe for issue %d '
338 'with labels of %s.') %
339 (issue.id, issue.labels))
340 # Close the bug, and do the search again, which should return None.
341 reporter.modify_bug_report(issue.id,
342 comment='Issue closed by test_push script.',
343 label_update='',
344 status='WontFix')
345 second_issue = reporter.find_issue_by_marker(BUG_ANCHOR)
346 if second_issue:
347 ids = '%d, %d' % (issue.id, second_issue.id)
348 raise TestPushException(('Auto bug file failed. Multiple issues (%s) '
349 'filed with marker %s') % (ids, BUG_ANCHOR))
350 print 'Issue %d was filed and deduped successfully.' % issue.id
351
352
353def main():
354 """Entry point for test_push script."""
355 arguments = parse_arguments()
356
357 try:
358 # Close existing bugs. New bug should be filed in dummy_Fail test.
359 old_issue_ids = close_bug()
360 test_suite(PUSH_TO_PROD_SUITE, EXPECTED_TEST_RESULTS, arguments)
361 check_bug_filed_and_deduped(old_issue_ids)
362
363 # TODO(dshi): Remove following line after crbug.com/267644 is fixed.
364 # Also, merge EXPECTED_TEST_RESULTS_AU to EXPECTED_TEST_RESULTS
365 test_suite(AU_SUITE, EXPECTED_TEST_RESULTS_AU, arguments)
366 except Exception as e:
367 print 'Test for pushing to prod failed:\n'
368 print str(e)
369 # Send out email about the test failure.
370 if arguments.email:
371 mail.send(MAIL_FROM,
372 [arguments.email],
373 [],
374 'Test for pushing to prod failed. Do NOT push!',
375 ('Errors occurred during the test:\n\n%s\n\n' % str(e) +
Dan Shie21a24e2014-02-26 09:53:32 -0800376 'run_suite output:\n\n%s' % '\n'.join(run_suite_output)))
Dan Shi7e04fa82013-07-25 15:08:48 -0700377 raise
378
379 message = ('\nAll tests are completed successfully, prod branch is ready to'
380 ' be pushed.')
381 print message
382 # Send out email about test completed successfully.
383 if arguments.email:
384 mail.send(MAIL_FROM,
385 [arguments.email],
386 [],
387 'Test for pushing to prod completed successfully',
388 message)
389
390
391if __name__ == '__main__':
392 sys.exit(main())