blob: 86d4ed30d570f27b1c9b7a39bfe867b5672c842a [file] [log] [blame]
Chris Masone24b80f12012-02-14 14:18:01 -08001#!/usr/bin/python
2#
3# Copyright (c) 2012 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 for running suites of tests and waiting for completion.
8
9The desired test suite will be scheduled with autotest, and then
10this tool will block until the job is complete, printing a summary
11at the end. Error conditions result in exceptions.
12
13This is intended for use only with Chrome OS test suits that leverage the
14dynamic suite infrastructure in server/cros/dynamic_suite.py.
15"""
16
Chris Masonecfa7efc2012-09-06 16:00:07 -070017import getpass, hashlib, logging, optparse, os, time, sys
18from datetime import datetime
19
Chris Masone24b80f12012-02-14 14:18:01 -080020import common
Chris Masonecfa7efc2012-09-06 16:00:07 -070021
Simran Basi22aa9fe2012-12-07 16:37:09 -080022from autotest_lib.client.common_lib import global_config, error, utils
Chris Masone44e4d6c2012-08-15 14:25:53 -070023from autotest_lib.server.cros.dynamic_suite import constants
Chris Masoneb4935552012-08-14 12:05:54 -070024from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
25from autotest_lib.server.cros.dynamic_suite import job_status
Chris Masoned9f13c52012-08-29 10:37:08 -070026from autotest_lib.server.cros.dynamic_suite.reimager import Reimager
Chris Masone24b80f12012-02-14 14:18:01 -080027
Chris Masone1120cdf2012-02-27 17:35:07 -080028CONFIG = global_config.global_config
29
Simran Basi22aa9fe2012-12-07 16:37:09 -080030# Return code that will be sent back to autotest_rpc_server.py
31OK = 0
32ERROR = 1
33WARNING = 2
34
Chris Masonedfa0beba2012-03-19 11:41:47 -070035
Scott Zawalski94457b72012-07-02 18:45:07 -040036def setup_logging(logfile=None):
37 """Setup basic logging with all logging info stripped.
38
39 Calls to logging will only show the message. No severity is logged.
40
41 @param logfile: If specified dump output to a file as well.
42 """
43 screen_handler = logging.StreamHandler()
44 screen_handler.setFormatter(logging.Formatter('%(message)s'))
45 logging.getLogger().addHandler(screen_handler)
46 logging.getLogger().setLevel(logging.INFO)
47 if logfile:
48 file_handler = logging.FileHandler(logfile)
49 file_handler.setLevel(logging.DEBUG)
50 logging.getLogger().addHandler(file_handler)
Chris Masonedfa0beba2012-03-19 11:41:47 -070051
52
Chris Masone24b80f12012-02-14 14:18:01 -080053def parse_options():
Zdenek Behan77290c32012-06-26 17:39:47 +020054 usage = "usage: %prog [options]"
Chris Masone24b80f12012-02-14 14:18:01 -080055 parser = optparse.OptionParser(usage=usage)
56 parser.add_option("-b", "--board", dest="board")
57 parser.add_option("-i", "--build", dest="build")
Chris Masone359c0fd2012-03-13 15:18:59 -070058 # This should just be a boolean flag, but the autotest "proxy" code
59 # can't handle flags that don't take arguments.
Alex Millerab33ddb2012-10-03 12:56:02 -070060 parser.add_option("-n", "--no_wait", dest="no_wait", default="False",
61 help='Must pass "True" or "False" if used.')
Scott Zawalski65650172012-02-16 11:48:26 -050062 parser.add_option("-p", "--pool", dest="pool", default=None)
Chris Masone24b80f12012-02-14 14:18:01 -080063 parser.add_option("-s", "--suite_name", dest="name")
Chris Masone8ac66712012-02-15 14:21:02 -080064 parser.add_option("-t", "--timeout_min", dest="timeout_min", default=30)
65 parser.add_option("-d", "--delay_sec", dest="delay_sec", default=10)
Chris Masone986459e2012-04-11 11:36:48 -070066 parser.add_option("-m", "--mock_job_id", dest="mock_job_id",
67 help="Skips running suite; creates report for given ID.")
Alex Miller05a2fff2012-09-10 10:14:34 -070068 parser.add_option("-u", "--num", dest="num", type="int", default=None,
Chris Masone8906ab12012-07-23 15:37:56 -070069 help="Run on at most NUM machines.")
Alex Millerf43d0eb2012-10-01 13:43:13 -070070 # Same boolean flag issue applies here.
Alex Millerab33ddb2012-10-03 12:56:02 -070071 parser.add_option("-f", "--file_bugs", dest="file_bugs", default='False',
72 help='File bugs on test failures. Must pass "True" or '
73 '"False" if used.')
Alex Millerf43d0eb2012-10-01 13:43:13 -070074
75
Chris Masone24b80f12012-02-14 14:18:01 -080076 options, args = parser.parse_args()
77 return parser, options, args
78
79
80def get_pretty_status(status):
81 if status == 'GOOD':
82 return '[ PASSED ]'
Chris Masone8906ab12012-07-23 15:37:56 -070083 elif status == 'TEST_NA':
84 return '[ INFO ]'
Chris Masone24b80f12012-02-14 14:18:01 -080085 return '[ FAILED ]'
86
Zdenek Behan150fbd62012-04-06 17:20:01 +020087def is_fail_status(status):
88 # All the statuses tests can have when they fail.
Chris Masone8906ab12012-07-23 15:37:56 -070089 if status in ['FAIL', 'ERROR']:
Zdenek Behan150fbd62012-04-06 17:20:01 +020090 return True
91 return False
92
Chris Masone24b80f12012-02-14 14:18:01 -080093
Zdenek Behan150fbd62012-04-06 17:20:01 +020094def get_view_info(suite_job_id, view):
95 """
96 Parse a view for the slave job name and job_id.
97
98 @param suite_job_id: The job id of our master suite job.
99 @param view: Test result view.
100 @return A tuple job_name, experimental of the slave test run
101 described by view.
102 """
103 # By default, we are the main suite job since there is no
104 # keyval entry for our job_name.
105 job_name = '%s-%s' % (suite_job_id, getpass.getuser())
106 experimental = False
107 if 'job_keyvals' in view:
108 # The job name depends on whether it's experimental or not.
Chris Masoned9f13c52012-08-29 10:37:08 -0700109 if view['test_name'].startswith(Reimager.JOB_NAME):
110 std_job_name = Reimager.JOB_NAME
111 elif job_status.view_is_for_infrastructure_fail(view):
112 std_job_name = view['test_name']
113 else:
114 std_job_name = view['test_name'].split('.')[0]
Chris Masone44e4d6c2012-08-15 14:25:53 -0700115 exp_job_name = constants.EXPERIMENTAL_PREFIX + std_job_name
Chris Masone11aae452012-05-21 16:08:39 -0700116
Chris Masoned9f13c52012-08-29 10:37:08 -0700117 std_job_hash = hashlib.md5(std_job_name).hexdigest()
118 exp_job_hash = hashlib.md5(exp_job_name).hexdigest()
Chris Masone11aae452012-05-21 16:08:39 -0700119
120 if std_job_hash in view['job_keyvals']:
121 job_name = view['job_keyvals'][std_job_hash]
122 elif exp_job_hash in view['job_keyvals']:
123 experimental = True
124 job_name = view['job_keyvals'][exp_job_hash]
125
126 # For backward compatibility.
Zdenek Behan150fbd62012-04-06 17:20:01 +0200127 if std_job_name in view['job_keyvals']:
128 job_name = view['job_keyvals'][std_job_name]
129 elif exp_job_name in view['job_keyvals']:
130 experimental = True
131 job_name = view['job_keyvals'][exp_job_name]
Chris Masone11aae452012-05-21 16:08:39 -0700132
Zdenek Behan150fbd62012-04-06 17:20:01 +0200133 return job_name, experimental
134
135
Craig Harrison25eb0f32012-08-23 16:48:49 -0700136class LogLink(object):
137 """
138 Link to a log.
139
140 @var anchor: the link text.
141 @var url: the link url.
142 """
143 def __init__(self, anchor, job_string):
144 """
145 Initialize the LogLink by generating the log URL.
146
147 @param anchor: the link text.
148 @param job_string: the job whose logs we'd like to link to.
149 """
150 self.anchor = anchor
151 host = CONFIG.get_config_value('SERVER', 'hostname', type=str)
152 pattern = CONFIG.get_config_value('CROS', 'log_url_pattern', type=str)
153 self.url = pattern % (host, job_string)
154
155
156 def GenerateBuildbotLink(self):
157 """
158 Generate a link to the job's logs, for consumption by buildbot.
159
160 @return A link formatted for the buildbot log annotator.
161 """
Craig Harrisond8451572012-08-31 10:29:33 -0700162 return "@@@STEP_LINK@%s@%s@@@" % (self.anchor.strip(), self.url)
Craig Harrison25eb0f32012-08-23 16:48:49 -0700163
164
Craig Harrisond8451572012-08-31 10:29:33 -0700165 def GenerateTextLink(self):
Craig Harrison25eb0f32012-08-23 16:48:49 -0700166 """
Craig Harrisond8451572012-08-31 10:29:33 -0700167 Generate a link to the job's logs, for consumption by a human.
Craig Harrison25eb0f32012-08-23 16:48:49 -0700168
Craig Harrisond8451572012-08-31 10:29:33 -0700169 @return A link formatted for human readability.
Craig Harrison25eb0f32012-08-23 16:48:49 -0700170 """
Craig Harrisond8451572012-08-31 10:29:33 -0700171 return "%s%s" % (self.anchor, self.url)
Craig Harrison25eb0f32012-08-23 16:48:49 -0700172
173
Chris Masoneb61b4052012-04-30 14:35:28 -0700174class Timings(object):
175 """Timings for important events during a suite.
176
177 All timestamps are datetime.datetime objects.
178
179 @var suite_start_time: the time the suite started.
180 @var reimage_start_time: the time we started reimaging devices.
181 @var reimage_end_time: the time we finished reimaging devices.
182 @var tests_start_time: the time the first test started running.
183 """
Chris Masonea8066a92012-05-01 16:52:31 -0700184 download_start_time = None
185 payload_end_time = None
186 artifact_end_time = None
Chris Masoneb61b4052012-04-30 14:35:28 -0700187 suite_start_time = None
Chris Masone604baf32012-06-28 08:45:30 -0700188 reimage_times = {} # {'hostname': (start_time, end_time)}
Chris Masoneb61b4052012-04-30 14:35:28 -0700189 tests_start_time = None
190 tests_end_time = None
191
192
Chris Masoned9f13c52012-08-29 10:37:08 -0700193 def RecordTiming(self, view):
194 """Given a test report view, extract and record pertinent time info.
Chris Masoneb61b4052012-04-30 14:35:28 -0700195
196 get_detailed_test_views() returns a list of entries that provide
197 info about the various parts of a suite run. This method can take
198 any one of these entries and look up timestamp info we might want
199 and record it.
200
Chris Masonecfa7efc2012-09-06 16:00:07 -0700201 If timestamps are unavailable, datetime.datetime.min/max will be used.
202
Chris Masoned9f13c52012-08-29 10:37:08 -0700203 @param view: a view dict, as returned by get_detailed_test_views().
Chris Masoneb61b4052012-04-30 14:35:28 -0700204 """
Chris Masonecfa7efc2012-09-06 16:00:07 -0700205 start_candidate = datetime.min
206 end_candidate = datetime.max
207 if view['test_started_time']:
208 start_candidate = datetime.strptime(view['test_started_time'],
209 job_status.TIME_FMT)
210 if view['test_finished_time']:
211 end_candidate = datetime.strptime(view['test_finished_time'],
212 job_status.TIME_FMT)
213
Chris Masoned9f13c52012-08-29 10:37:08 -0700214 if job_status.view_is_for_suite_prep(view):
Chris Masoneb61b4052012-04-30 14:35:28 -0700215 self.suite_start_time = start_candidate
Chris Masoned9f13c52012-08-29 10:37:08 -0700216 elif view['test_name'].startswith(Reimager.JOB_NAME):
217 if '-' in view['test_name']:
218 hostname = view['test_name'].split('-', 1)[1]
Chris Masonef76ebba2012-06-28 14:56:22 -0700219 else:
220 hostname = ''
Chris Masone604baf32012-06-28 08:45:30 -0700221 self.reimage_times[hostname] = (start_candidate, end_candidate)
Chris Masoneb61b4052012-04-30 14:35:28 -0700222 else:
223 self._UpdateFirstTestStartTime(start_candidate)
224 self._UpdateLastTestEndTime(end_candidate)
Chris Masoned9f13c52012-08-29 10:37:08 -0700225 if 'job_keyvals' in view:
226 keyvals = view['job_keyvals']
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700227 self.download_start_time = keyvals.get(
Chris Masone44e4d6c2012-08-15 14:25:53 -0700228 constants.DOWNLOAD_STARTED_TIME)
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700229 self.payload_end_time = keyvals.get(
Chris Masone44e4d6c2012-08-15 14:25:53 -0700230 constants.PAYLOAD_FINISHED_TIME)
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700231 self.artifact_end_time = keyvals.get(
Chris Masone44e4d6c2012-08-15 14:25:53 -0700232 constants.ARTIFACT_FINISHED_TIME)
233
Chris Masoneb61b4052012-04-30 14:35:28 -0700234
235 def _UpdateFirstTestStartTime(self, candidate):
236 """Update self.tests_start_time, iff candidate is an earlier time.
237
238 @param candidate: a datetime.datetime object.
239 """
240 if not self.tests_start_time or candidate < self.tests_start_time:
241 self.tests_start_time = candidate
242
243
244 def _UpdateLastTestEndTime(self, candidate):
245 """Update self.tests_end_time, iff candidate is a later time.
246
247 @param candidate: a datetime.datetime object.
248 """
249 if not self.tests_end_time or candidate > self.tests_end_time:
250 self.tests_end_time = candidate
251
252
253 def __str__(self):
Chris Masone604baf32012-06-28 08:45:30 -0700254 reimaging_info = ''
255 for host, (start, end) in self.reimage_times.iteritems():
256 reimaging_info += ('Reimaging %s started at %s\n'
257 'Reimaging %s ended at %s\n' % (host, start,
258 host, end))
Chris Masoneb61b4052012-04-30 14:35:28 -0700259 return ('\n'
260 'Suite timings:\n'
Chris Masonea8066a92012-05-01 16:52:31 -0700261 'Downloads started at %s\n'
262 'Payload downloads ended at %s\n'
Chris Masoneb61b4052012-04-30 14:35:28 -0700263 'Suite started at %s\n'
Chris Masone604baf32012-06-28 08:45:30 -0700264 '%s'
Chris Masonea8066a92012-05-01 16:52:31 -0700265 'Artifact downloads ended (at latest) at %s\n'
Chris Masoneb61b4052012-04-30 14:35:28 -0700266 'Testing started at %s\n'
Chris Masonea8066a92012-05-01 16:52:31 -0700267 'Testing ended at %s\n' % (self.download_start_time,
268 self.payload_end_time,
269 self.suite_start_time,
Chris Masone604baf32012-06-28 08:45:30 -0700270 reimaging_info,
Chris Masonea8066a92012-05-01 16:52:31 -0700271 self.artifact_end_time,
Chris Masoneb61b4052012-04-30 14:35:28 -0700272 self.tests_start_time,
273 self.tests_end_time))
274
275
Craig Harrisond8451572012-08-31 10:29:33 -0700276def _full_test_name(job_id, view):
277 """Generates the full test name for printing to logs.
278
279 @param job_id: the job id.
280 @param view: the view for which we are generating the name.
281 @return The test name, possibly with a descriptive prefix appended.
282 """
283 job_name, experimental = get_view_info(job_id, view)
284 prefix = constants.EXPERIMENTAL_PREFIX if experimental else ''
285 return prefix + view['test_name']
286
287
Chris Masone24b80f12012-02-14 14:18:01 -0800288def main():
289 parser, options, args = parse_options()
Chris Masone3a850642012-07-11 11:11:18 -0700290 log_name = 'run_suite-default.log'
Chris Masone986459e2012-04-11 11:36:48 -0700291 if not options.mock_job_id:
Zdenek Behan77290c32012-06-26 17:39:47 +0200292 if args:
293 print 'Unknown arguments: ' + str(args)
294 parser.print_help()
295 return
296 if not options.build:
297 print 'Need to specify which build to use'
298 parser.print_help()
299 return
300 if not options.board:
301 print 'Need to specify board'
302 parser.print_help()
303 return
304 if not options.name:
305 print 'Need to specify suite name'
Chris Masone986459e2012-04-11 11:36:48 -0700306 parser.print_help()
307 return
Chris Masone3a850642012-07-11 11:11:18 -0700308 # convert build name from containing / to containing only _
309 log_name = 'run_suite-%s.log' % options.build.replace('/', '_')
310 log_dir = os.path.join(common.autotest_dir, 'logs')
311 if os.path.exists(log_dir):
312 log_name = os.path.join(log_dir, log_name)
Alex Miller8e75d0d2012-07-31 15:13:32 -0700313 if options.num is not None and options.num < 1:
314 print 'Number of machines must be more than 0, if specified.'
315 parser.print_help()
316 return
Alex Millerab33ddb2012-10-03 12:56:02 -0700317 if options.no_wait != 'True' and options.no_wait != 'False':
318 print 'Please specify "True" or "False" for --no_wait.'
319 parser.print_help()
320 return
321 if options.file_bugs != 'True' and options.file_bugs != 'False':
322 print 'Please specify "True" or "False" for --file_bugs.'
323 parser.print_help()
324 return
Scott Zawalski94457b72012-07-02 18:45:07 -0400325 setup_logging(logfile=log_name)
Chris Masonedfa0beba2012-03-19 11:41:47 -0700326
Simran Basi22aa9fe2012-12-07 16:37:09 -0800327 try:
328 utils.check_lab_status()
329 except error.LabIsDownException as e:
330 # Lab is not up, return WARNING.
331 logging.debug('Lab is not up. Error message: %s', e)
332 print str(e)
333 return WARNING
334
Chris Masone8ac66712012-02-15 14:21:02 -0800335 afe = frontend_wrappers.RetryingAFE(timeout_min=options.timeout_min,
336 delay_sec=options.delay_sec)
Chris Masone359c0fd2012-03-13 15:18:59 -0700337
Alex Millerab33ddb2012-10-03 12:56:02 -0700338 wait = (options.no_wait == 'False')
339 file_bugs = (options.file_bugs == 'True')
Chris Masone986459e2012-04-11 11:36:48 -0700340 if options.mock_job_id:
341 job_id = int(options.mock_job_id)
342 else:
Alex Miller8e75d0d2012-07-31 15:13:32 -0700343 job_id = afe.run('create_suite_job', suite_name=options.name,
344 board=options.board, build=options.build,
Alex Millerf43d0eb2012-10-01 13:43:13 -0700345 check_hosts=wait, pool=options.pool, num=options.num,
346 file_bugs=file_bugs)
Chris Masone8ac66712012-02-15 14:21:02 -0800347 TKO = frontend_wrappers.RetryingTKO(timeout_min=options.timeout_min,
348 delay_sec=options.delay_sec)
Scott Zawalski94457b72012-07-02 18:45:07 -0400349 logging.info('Started suite job: %s', job_id)
Simran Basi22aa9fe2012-12-07 16:37:09 -0800350
351 code = OK
Chris Masone359c0fd2012-03-13 15:18:59 -0700352 while wait and True:
Chris Masone24b80f12012-02-14 14:18:01 -0800353 if not afe.get_jobs(id=job_id, finished=True):
354 time.sleep(1)
355 continue
Scott Zawalski0acfe112012-03-06 09:21:44 -0500356 views = TKO.run('get_detailed_test_views', afe_job_id=job_id)
Craig Harrisond8451572012-08-31 10:29:33 -0700357 width = max((len(_full_test_name(job_id, view)) for view in views)) + 3
Scott Zawalski0acfe112012-03-06 09:21:44 -0500358
Chris Masoned9f13c52012-08-29 10:37:08 -0700359 relevant_views = filter(job_status.view_is_relevant, views)
Scott Zawalski0acfe112012-03-06 09:21:44 -0500360 if not relevant_views:
Zdenek Behan150fbd62012-04-06 17:20:01 +0200361 # The main suite job most likely failed in SERVER_JOB.
362 relevant_views = views
Scott Zawalski0acfe112012-03-06 09:21:44 -0500363
Chris Masoneb61b4052012-04-30 14:35:28 -0700364 timings = Timings()
Craig Harrison25eb0f32012-08-23 16:48:49 -0700365 web_links = []
366 buildbot_links = []
Chris Masoned9f13c52012-08-29 10:37:08 -0700367 for view in relevant_views:
368 timings.RecordTiming(view)
369 if job_status.view_is_for_suite_prep(view):
370 view['test_name'] = 'Suite prep'
Chris Masone3a850642012-07-11 11:11:18 -0700371
Chris Masoned9f13c52012-08-29 10:37:08 -0700372 job_name, experimental = get_view_info(job_id, view)
Craig Harrisond8451572012-08-31 10:29:33 -0700373 test_view = _full_test_name(job_id, view).ljust(width)
Chris Masoned9f13c52012-08-29 10:37:08 -0700374 logging.info("%s%s", test_view, get_pretty_status(view['status']))
Craig Harrisond8451572012-08-31 10:29:33 -0700375 link = LogLink(test_view, job_name)
Craig Harrison25eb0f32012-08-23 16:48:49 -0700376 web_links.append(link)
Chris Masoned9f13c52012-08-29 10:37:08 -0700377
378 if view['status'] != 'GOOD':
379 logging.info("%s %s: %s", test_view, view['status'],
380 view['reason'])
Craig Harrison25eb0f32012-08-23 16:48:49 -0700381 # Don't show links on the buildbot waterfall for tests with
382 # GOOD status.
383 buildbot_links.append(link)
Chris Masone8906ab12012-07-23 15:37:56 -0700384 if view['status'] == 'TEST_NA':
385 # Didn't run; nothing to do here!
386 continue
Simran Basi22aa9fe2012-12-07 16:37:09 -0800387 if code == ERROR:
Zdenek Behan150fbd62012-04-06 17:20:01 +0200388 # Failed already, no need to worry further.
389 continue
Chris Masoned9f13c52012-08-29 10:37:08 -0700390 if (view['status'] == 'WARN' or
391 (is_fail_status(view['status']) and experimental)):
Zdenek Behan150fbd62012-04-06 17:20:01 +0200392 # Failures that produce a warning. Either a test with WARN
393 # status or any experimental test failure.
Simran Basi22aa9fe2012-12-07 16:37:09 -0800394 code = WARNING
Chris Masone5374c672012-03-05 15:11:39 -0800395 else:
Simran Basi22aa9fe2012-12-07 16:37:09 -0800396 code = ERROR
Scott Zawalski94457b72012-07-02 18:45:07 -0400397 logging.info(timings)
Craig Harrison25eb0f32012-08-23 16:48:49 -0700398 logging.info('\n'
399 'Links to test logs:')
400 for link in web_links:
Craig Harrisond8451572012-08-31 10:29:33 -0700401 logging.info(link.GenerateTextLink())
Craig Harrison25eb0f32012-08-23 16:48:49 -0700402 logging.info('\n'
403 'Output below this line is for buildbot consumption:')
404 for link in buildbot_links:
405 logging.info(link.GenerateBuildbotLink())
Chris Masone24b80f12012-02-14 14:18:01 -0800406 break
Chris Masoned5939fe2012-03-13 10:11:06 -0700407 else:
Scott Zawalski94457b72012-07-02 18:45:07 -0400408 logging.info('Created suite job: %r', job_id)
Craig Harrison25eb0f32012-08-23 16:48:49 -0700409 link = LogLink(options.name, '%s-%s' % (job_id, getpass.getuser()))
410 logging.info(link.GenerateBuildbotLink())
Scott Zawalski94457b72012-07-02 18:45:07 -0400411 logging.info('--no_wait specified; Exiting.')
Chris Masone24b80f12012-02-14 14:18:01 -0800412 return code
413
414if __name__ == "__main__":
415 sys.exit(main())