blob: e5144f55690d1d950dfc0f5493714af3f5de5a07 [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 Masone11aae452012-05-21 16:08:39 -070017import datetime, getpass, hashlib, optparse, time, sys
Chris Masone24b80f12012-02-14 14:18:01 -080018import common
Chris Masonedfa0beba2012-03-19 11:41:47 -070019import logging
Chris Masone1120cdf2012-02-27 17:35:07 -080020from autotest_lib.client.common_lib import global_config
Chris Masoneaa10f8e2012-05-15 13:34:21 -070021from autotest_lib.server.cros import dynamic_suite, frontend_wrappers
Chris Masone8d6e6412012-06-28 11:20:56 -070022from autotest_lib.server.cros import job_status
Chris Masonedfa0beba2012-03-19 11:41:47 -070023from autotest_lib.client.common_lib import logging_config, logging_manager
Chris Masone24b80f12012-02-14 14:18:01 -080024
Chris Masone1120cdf2012-02-27 17:35:07 -080025CONFIG = global_config.global_config
26
Chris Masonedfa0beba2012-03-19 11:41:47 -070027
28class RunSuiteLoggingConfig(logging_config.LoggingConfig):
29 def configure_logging(self, verbose=False):
30 super(RunSuiteLoggingConfig, self).configure_logging(use_console=True)
31
32
Chris Masone24b80f12012-02-14 14:18:01 -080033def parse_options():
Zdenek Behan77290c32012-06-26 17:39:47 +020034 usage = "usage: %prog [options]"
Chris Masone24b80f12012-02-14 14:18:01 -080035 parser = optparse.OptionParser(usage=usage)
36 parser.add_option("-b", "--board", dest="board")
37 parser.add_option("-i", "--build", dest="build")
Chris Masone359c0fd2012-03-13 15:18:59 -070038 # This should just be a boolean flag, but the autotest "proxy" code
39 # can't handle flags that don't take arguments.
40 parser.add_option("-n", "--no_wait", dest="no_wait", default=None)
Scott Zawalski65650172012-02-16 11:48:26 -050041 parser.add_option("-p", "--pool", dest="pool", default=None)
Chris Masone24b80f12012-02-14 14:18:01 -080042 parser.add_option("-s", "--suite_name", dest="name")
Chris Masone8ac66712012-02-15 14:21:02 -080043 parser.add_option("-t", "--timeout_min", dest="timeout_min", default=30)
44 parser.add_option("-d", "--delay_sec", dest="delay_sec", default=10)
Chris Masone986459e2012-04-11 11:36:48 -070045 parser.add_option("-m", "--mock_job_id", dest="mock_job_id",
46 help="Skips running suite; creates report for given ID.")
Chris Masone24b80f12012-02-14 14:18:01 -080047 options, args = parser.parse_args()
48 return parser, options, args
49
50
51def get_pretty_status(status):
52 if status == 'GOOD':
53 return '[ PASSED ]'
54 return '[ FAILED ]'
55
Zdenek Behan150fbd62012-04-06 17:20:01 +020056def is_fail_status(status):
57 # All the statuses tests can have when they fail.
58 if status in ['FAIL', 'ERROR', 'TEST_NA']:
59 return True
60 return False
61
Chris Masone24b80f12012-02-14 14:18:01 -080062
63def status_is_relevant(status):
64 """
65 Indicates whether the status of a given test is meaningful or not.
66
67 @param status: frontend.TestStatus object to look at.
68 @return True if this is a test result worth looking at further.
69 """
Chris Masone986459e2012-04-11 11:36:48 -070070 return not status['test_name'].startswith('CLIENT_JOB')
Chris Masone24b80f12012-02-14 14:18:01 -080071
72
Chris Masone1120cdf2012-02-27 17:35:07 -080073def generate_log_link(anchor, job_string):
74 """
75 Generate a link to this job's logs, for consumption by buildbot.
76
77 @param anchor: Link anchor text.
78 @param job_id: the job whose logs we'd like to link to.
79 @return A link formatted for the buildbot log annotator.
80 """
81 host = CONFIG.get_config_value('SERVER', 'hostname', type=str)
82 pattern = CONFIG.get_config_value('CROS', 'log_url_pattern', type=str)
83 return "@@@STEP_LINK@%s@%s@@@" % (anchor, pattern % (host, job_string))
84
85
Zdenek Behan150fbd62012-04-06 17:20:01 +020086def get_view_info(suite_job_id, view):
87 """
88 Parse a view for the slave job name and job_id.
89
90 @param suite_job_id: The job id of our master suite job.
91 @param view: Test result view.
92 @return A tuple job_name, experimental of the slave test run
93 described by view.
94 """
95 # By default, we are the main suite job since there is no
96 # keyval entry for our job_name.
97 job_name = '%s-%s' % (suite_job_id, getpass.getuser())
98 experimental = False
99 if 'job_keyvals' in view:
100 # The job name depends on whether it's experimental or not.
101 std_job_name = view['test_name'].split('.')[0]
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700102 exp_job_name = dynamic_suite.EXPERIMENTAL_PREFIX + std_job_name
Chris Masone11aae452012-05-21 16:08:39 -0700103
104 std_job_hash = hashlib.md5(view['test_name'].split('.')[0]).hexdigest()
105 exp_job_hash = hashlib.md5(dynamic_suite.EXPERIMENTAL_PREFIX +
106 std_job_name).hexdigest()
107
108 if std_job_hash in view['job_keyvals']:
109 job_name = view['job_keyvals'][std_job_hash]
110 elif exp_job_hash in view['job_keyvals']:
111 experimental = True
112 job_name = view['job_keyvals'][exp_job_hash]
113
114 # For backward compatibility.
Zdenek Behan150fbd62012-04-06 17:20:01 +0200115 if std_job_name in view['job_keyvals']:
116 job_name = view['job_keyvals'][std_job_name]
117 elif exp_job_name in view['job_keyvals']:
118 experimental = True
119 job_name = view['job_keyvals'][exp_job_name]
Chris Masone11aae452012-05-21 16:08:39 -0700120
Zdenek Behan150fbd62012-04-06 17:20:01 +0200121 return job_name, experimental
122
123
Chris Masoneb61b4052012-04-30 14:35:28 -0700124class Timings(object):
125 """Timings for important events during a suite.
126
127 All timestamps are datetime.datetime objects.
128
129 @var suite_start_time: the time the suite started.
130 @var reimage_start_time: the time we started reimaging devices.
131 @var reimage_end_time: the time we finished reimaging devices.
132 @var tests_start_time: the time the first test started running.
133 """
Chris Masonea8066a92012-05-01 16:52:31 -0700134 download_start_time = None
135 payload_end_time = None
136 artifact_end_time = None
Chris Masoneb61b4052012-04-30 14:35:28 -0700137 suite_start_time = None
Chris Masone604baf32012-06-28 08:45:30 -0700138 reimage_times = {} # {'hostname': (start_time, end_time)}
Chris Masoneb61b4052012-04-30 14:35:28 -0700139 tests_start_time = None
140 tests_end_time = None
141
142
143 def RecordTiming(self, entry):
144 """Given a test report entry, extract and record pertinent time info.
145
146 get_detailed_test_views() returns a list of entries that provide
147 info about the various parts of a suite run. This method can take
148 any one of these entries and look up timestamp info we might want
149 and record it.
150
151 @param entry: an entry dict, as returned by get_details_test_views().
152 """
Chris Masoneb61b4052012-04-30 14:35:28 -0700153 start_candidate = datetime.datetime.strptime(entry['test_started_time'],
Chris Masone8d6e6412012-06-28 11:20:56 -0700154 job_status.TIME_FMT)
Chris Masoneb61b4052012-04-30 14:35:28 -0700155 end_candidate = datetime.datetime.strptime(entry['test_finished_time'],
Chris Masone8d6e6412012-06-28 11:20:56 -0700156 job_status.TIME_FMT)
Chris Masoneb61b4052012-04-30 14:35:28 -0700157 if entry['test_name'] == 'SERVER_JOB':
158 self.suite_start_time = start_candidate
Chris Masone604baf32012-06-28 08:45:30 -0700159 elif entry['test_name'].startswith('try_new_image'):
160 hostname = entry['test_name'].split('-', 1)[1]
161 self.reimage_times[hostname] = (start_candidate, end_candidate)
Chris Masoneb61b4052012-04-30 14:35:28 -0700162 else:
163 self._UpdateFirstTestStartTime(start_candidate)
164 self._UpdateLastTestEndTime(end_candidate)
Chris Masonea8066a92012-05-01 16:52:31 -0700165 if 'job_keyvals' in entry:
166 keyvals = entry['job_keyvals']
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700167 self.download_start_time = keyvals.get(
168 dynamic_suite.DOWNLOAD_STARTED_TIME)
169 self.payload_end_time = keyvals.get(
170 dynamic_suite.PAYLOAD_FINISHED_TIME)
171 self.artifact_end_time = keyvals.get(
172 dynamic_suite.ARTIFACT_FINISHED_TIME)
Chris Masoneb61b4052012-04-30 14:35:28 -0700173
174 def _UpdateFirstTestStartTime(self, candidate):
175 """Update self.tests_start_time, iff candidate is an earlier time.
176
177 @param candidate: a datetime.datetime object.
178 """
179 if not self.tests_start_time or candidate < self.tests_start_time:
180 self.tests_start_time = candidate
181
182
183 def _UpdateLastTestEndTime(self, candidate):
184 """Update self.tests_end_time, iff candidate is a later time.
185
186 @param candidate: a datetime.datetime object.
187 """
188 if not self.tests_end_time or candidate > self.tests_end_time:
189 self.tests_end_time = candidate
190
191
192 def __str__(self):
Chris Masone604baf32012-06-28 08:45:30 -0700193 reimaging_info = ''
194 for host, (start, end) in self.reimage_times.iteritems():
195 reimaging_info += ('Reimaging %s started at %s\n'
196 'Reimaging %s ended at %s\n' % (host, start,
197 host, end))
Chris Masoneb61b4052012-04-30 14:35:28 -0700198 return ('\n'
199 'Suite timings:\n'
Chris Masonea8066a92012-05-01 16:52:31 -0700200 'Downloads started at %s\n'
201 'Payload downloads ended at %s\n'
Chris Masoneb61b4052012-04-30 14:35:28 -0700202 'Suite started at %s\n'
Chris Masone604baf32012-06-28 08:45:30 -0700203 '%s'
Chris Masonea8066a92012-05-01 16:52:31 -0700204 'Artifact downloads ended (at latest) at %s\n'
Chris Masoneb61b4052012-04-30 14:35:28 -0700205 'Testing started at %s\n'
Chris Masonea8066a92012-05-01 16:52:31 -0700206 'Testing ended at %s\n' % (self.download_start_time,
207 self.payload_end_time,
208 self.suite_start_time,
Chris Masone604baf32012-06-28 08:45:30 -0700209 reimaging_info,
Chris Masonea8066a92012-05-01 16:52:31 -0700210 self.artifact_end_time,
Chris Masoneb61b4052012-04-30 14:35:28 -0700211 self.tests_start_time,
212 self.tests_end_time))
213
214
Chris Masone24b80f12012-02-14 14:18:01 -0800215def main():
216 parser, options, args = parse_options()
Chris Masone986459e2012-04-11 11:36:48 -0700217 if not options.mock_job_id:
Zdenek Behan77290c32012-06-26 17:39:47 +0200218 if args:
219 print 'Unknown arguments: ' + str(args)
220 parser.print_help()
221 return
222 if not options.build:
223 print 'Need to specify which build to use'
224 parser.print_help()
225 return
226 if not options.board:
227 print 'Need to specify board'
228 parser.print_help()
229 return
230 if not options.name:
231 print 'Need to specify suite name'
Chris Masone986459e2012-04-11 11:36:48 -0700232 parser.print_help()
233 return
Chris Masonedfa0beba2012-03-19 11:41:47 -0700234
235 logging_manager.configure_logging(RunSuiteLoggingConfig())
236
Chris Masone8ac66712012-02-15 14:21:02 -0800237 afe = frontend_wrappers.RetryingAFE(timeout_min=options.timeout_min,
238 delay_sec=options.delay_sec)
Chris Masone359c0fd2012-03-13 15:18:59 -0700239
240 wait = options.no_wait is None
Chris Masone986459e2012-04-11 11:36:48 -0700241 if options.mock_job_id:
242 job_id = int(options.mock_job_id)
243 else:
244 job_id = afe.run('create_suite_job',
245 suite_name=options.name,
246 board=options.board,
247 build=options.build,
248 check_hosts=wait,
249 pool=options.pool)
Chris Masone8ac66712012-02-15 14:21:02 -0800250 TKO = frontend_wrappers.RetryingTKO(timeout_min=options.timeout_min,
251 delay_sec=options.delay_sec)
Chris Masone24b80f12012-02-14 14:18:01 -0800252 # Return code that will be sent back to autotest_rpc_server.py
Zdenek Behan150fbd62012-04-06 17:20:01 +0200253 # 0 = OK
254 # 1 = ERROR
255 # 2 = WARNING
Chris Masone24b80f12012-02-14 14:18:01 -0800256 code = 0
Chris Masone359c0fd2012-03-13 15:18:59 -0700257 while wait and True:
Chris Masone24b80f12012-02-14 14:18:01 -0800258 if not afe.get_jobs(id=job_id, finished=True):
259 time.sleep(1)
260 continue
Scott Zawalski0acfe112012-03-06 09:21:44 -0500261 views = TKO.run('get_detailed_test_views', afe_job_id=job_id)
Chris Masone24b80f12012-02-14 14:18:01 -0800262 width = len(max(map(lambda x: x['test_name'], views), key=len)) + 3
Scott Zawalski0acfe112012-03-06 09:21:44 -0500263
264 relevant_views = filter(status_is_relevant, views)
265 if not relevant_views:
Zdenek Behan150fbd62012-04-06 17:20:01 +0200266 # The main suite job most likely failed in SERVER_JOB.
267 relevant_views = views
Scott Zawalski0acfe112012-03-06 09:21:44 -0500268
Chris Masoneb61b4052012-04-30 14:35:28 -0700269 timings = Timings()
Chris Masone1120cdf2012-02-27 17:35:07 -0800270 log_links = []
Scott Zawalski0acfe112012-03-06 09:21:44 -0500271 for entry in relevant_views:
Chris Masoneb61b4052012-04-30 14:35:28 -0700272 timings.RecordTiming(entry)
Chris Masone986459e2012-04-11 11:36:48 -0700273 entry['test_name'] = entry['test_name'].replace('SERVER_JOB',
274 'Suite prep')
Chris Masone24b80f12012-02-14 14:18:01 -0800275 test_entry = entry['test_name'].ljust(width)
276 print "%s%s" % (test_entry, get_pretty_status(entry['status']))
277 if entry['status'] != 'GOOD':
278 print "%s %s: %s" % (test_entry,
279 entry['status'],
280 entry['reason'])
Zdenek Behan150fbd62012-04-06 17:20:01 +0200281 job_name, experimental = get_view_info(job_id, entry)
Scott Zawalski0acfe112012-03-06 09:21:44 -0500282
283 log_links.append(generate_log_link(entry['test_name'],
284 job_name))
Zdenek Behan150fbd62012-04-06 17:20:01 +0200285 if code == 1:
286 # Failed already, no need to worry further.
287 continue
288 if (entry['status'] == 'WARN' or
289 (is_fail_status(entry['status']) and experimental)):
290 # Failures that produce a warning. Either a test with WARN
291 # status or any experimental test failure.
Chris Masone5374c672012-03-05 15:11:39 -0800292 code = 2
293 else:
294 code = 1
Chris Masoneb61b4052012-04-30 14:35:28 -0700295 print timings
Chris Masone1120cdf2012-02-27 17:35:07 -0800296 for link in log_links:
297 print link
Chris Masone24b80f12012-02-14 14:18:01 -0800298 break
Chris Masoned5939fe2012-03-13 10:11:06 -0700299 else:
300 print "Created suite job: %r" % job_id
301 print generate_log_link(options.name,
302 '%s-%s' % (job_id, getpass.getuser()))
303 print "--no_wait specified; Exiting."
Chris Masone24b80f12012-02-14 14:18:01 -0800304 return code
305
306if __name__ == "__main__":
307 sys.exit(main())