blob: b4083140e8e491375227ebb2e0f1262c1ddd37dd [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
138 reimage_start_time = None
139 reimage_end_time = None
140 tests_start_time = None
141 tests_end_time = None
142
143
144 def RecordTiming(self, entry):
145 """Given a test report entry, extract and record pertinent time info.
146
147 get_detailed_test_views() returns a list of entries that provide
148 info about the various parts of a suite run. This method can take
149 any one of these entries and look up timestamp info we might want
150 and record it.
151
152 @param entry: an entry dict, as returned by get_details_test_views().
153 """
Chris Masoneb61b4052012-04-30 14:35:28 -0700154 start_candidate = datetime.datetime.strptime(entry['test_started_time'],
Chris Masone8d6e6412012-06-28 11:20:56 -0700155 job_status.TIME_FMT)
Chris Masoneb61b4052012-04-30 14:35:28 -0700156 end_candidate = datetime.datetime.strptime(entry['test_finished_time'],
Chris Masone8d6e6412012-06-28 11:20:56 -0700157 job_status.TIME_FMT)
Chris Masoneb61b4052012-04-30 14:35:28 -0700158 if entry['test_name'] == 'SERVER_JOB':
159 self.suite_start_time = start_candidate
160 elif entry['test_name'] == 'try_new_image':
161 self.reimage_start_time = start_candidate
162 self.reimage_end_time = end_candidate
163 else:
164 self._UpdateFirstTestStartTime(start_candidate)
165 self._UpdateLastTestEndTime(end_candidate)
Chris Masonea8066a92012-05-01 16:52:31 -0700166 if 'job_keyvals' in entry:
167 keyvals = entry['job_keyvals']
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700168 self.download_start_time = keyvals.get(
169 dynamic_suite.DOWNLOAD_STARTED_TIME)
170 self.payload_end_time = keyvals.get(
171 dynamic_suite.PAYLOAD_FINISHED_TIME)
172 self.artifact_end_time = keyvals.get(
173 dynamic_suite.ARTIFACT_FINISHED_TIME)
Chris Masoneb61b4052012-04-30 14:35:28 -0700174
175 def _UpdateFirstTestStartTime(self, candidate):
176 """Update self.tests_start_time, iff candidate is an earlier time.
177
178 @param candidate: a datetime.datetime object.
179 """
180 if not self.tests_start_time or candidate < self.tests_start_time:
181 self.tests_start_time = candidate
182
183
184 def _UpdateLastTestEndTime(self, candidate):
185 """Update self.tests_end_time, iff candidate is a later time.
186
187 @param candidate: a datetime.datetime object.
188 """
189 if not self.tests_end_time or candidate > self.tests_end_time:
190 self.tests_end_time = candidate
191
192
193 def __str__(self):
194 return ('\n'
195 'Suite timings:\n'
Chris Masonea8066a92012-05-01 16:52:31 -0700196 'Downloads started at %s\n'
197 'Payload downloads ended at %s\n'
Chris Masoneb61b4052012-04-30 14:35:28 -0700198 'Suite started at %s\n'
199 'Reimaging started at %s\n'
200 'Reimaging ended at %s\n'
Chris Masonea8066a92012-05-01 16:52:31 -0700201 'Artifact downloads ended (at latest) at %s\n'
Chris Masoneb61b4052012-04-30 14:35:28 -0700202 'Testing started at %s\n'
Chris Masonea8066a92012-05-01 16:52:31 -0700203 'Testing ended at %s\n' % (self.download_start_time,
204 self.payload_end_time,
205 self.suite_start_time,
Chris Masoneb61b4052012-04-30 14:35:28 -0700206 self.reimage_start_time,
207 self.reimage_end_time,
Chris Masonea8066a92012-05-01 16:52:31 -0700208 self.artifact_end_time,
Chris Masoneb61b4052012-04-30 14:35:28 -0700209 self.tests_start_time,
210 self.tests_end_time))
211
212
Chris Masone24b80f12012-02-14 14:18:01 -0800213def main():
214 parser, options, args = parse_options()
Chris Masone986459e2012-04-11 11:36:48 -0700215 if not options.mock_job_id:
Zdenek Behan77290c32012-06-26 17:39:47 +0200216 if args:
217 print 'Unknown arguments: ' + str(args)
218 parser.print_help()
219 return
220 if not options.build:
221 print 'Need to specify which build to use'
222 parser.print_help()
223 return
224 if not options.board:
225 print 'Need to specify board'
226 parser.print_help()
227 return
228 if not options.name:
229 print 'Need to specify suite name'
Chris Masone986459e2012-04-11 11:36:48 -0700230 parser.print_help()
231 return
Chris Masonedfa0beba2012-03-19 11:41:47 -0700232
233 logging_manager.configure_logging(RunSuiteLoggingConfig())
234
Chris Masone8ac66712012-02-15 14:21:02 -0800235 afe = frontend_wrappers.RetryingAFE(timeout_min=options.timeout_min,
236 delay_sec=options.delay_sec)
Chris Masone359c0fd2012-03-13 15:18:59 -0700237
238 wait = options.no_wait is None
Chris Masone986459e2012-04-11 11:36:48 -0700239 if options.mock_job_id:
240 job_id = int(options.mock_job_id)
241 else:
242 job_id = afe.run('create_suite_job',
243 suite_name=options.name,
244 board=options.board,
245 build=options.build,
246 check_hosts=wait,
247 pool=options.pool)
Chris Masone8ac66712012-02-15 14:21:02 -0800248 TKO = frontend_wrappers.RetryingTKO(timeout_min=options.timeout_min,
249 delay_sec=options.delay_sec)
Chris Masone24b80f12012-02-14 14:18:01 -0800250 # Return code that will be sent back to autotest_rpc_server.py
Zdenek Behan150fbd62012-04-06 17:20:01 +0200251 # 0 = OK
252 # 1 = ERROR
253 # 2 = WARNING
Chris Masone24b80f12012-02-14 14:18:01 -0800254 code = 0
Chris Masone359c0fd2012-03-13 15:18:59 -0700255 while wait and True:
Chris Masone24b80f12012-02-14 14:18:01 -0800256 if not afe.get_jobs(id=job_id, finished=True):
257 time.sleep(1)
258 continue
Scott Zawalski0acfe112012-03-06 09:21:44 -0500259 views = TKO.run('get_detailed_test_views', afe_job_id=job_id)
Chris Masone24b80f12012-02-14 14:18:01 -0800260 width = len(max(map(lambda x: x['test_name'], views), key=len)) + 3
Scott Zawalski0acfe112012-03-06 09:21:44 -0500261
262 relevant_views = filter(status_is_relevant, views)
263 if not relevant_views:
Zdenek Behan150fbd62012-04-06 17:20:01 +0200264 # The main suite job most likely failed in SERVER_JOB.
265 relevant_views = views
Scott Zawalski0acfe112012-03-06 09:21:44 -0500266
Chris Masoneb61b4052012-04-30 14:35:28 -0700267 timings = Timings()
Chris Masone1120cdf2012-02-27 17:35:07 -0800268 log_links = []
Scott Zawalski0acfe112012-03-06 09:21:44 -0500269 for entry in relevant_views:
Chris Masoneb61b4052012-04-30 14:35:28 -0700270 timings.RecordTiming(entry)
Chris Masone986459e2012-04-11 11:36:48 -0700271 entry['test_name'] = entry['test_name'].replace('SERVER_JOB',
272 'Suite prep')
Chris Masone24b80f12012-02-14 14:18:01 -0800273 test_entry = entry['test_name'].ljust(width)
274 print "%s%s" % (test_entry, get_pretty_status(entry['status']))
275 if entry['status'] != 'GOOD':
276 print "%s %s: %s" % (test_entry,
277 entry['status'],
278 entry['reason'])
Zdenek Behan150fbd62012-04-06 17:20:01 +0200279 job_name, experimental = get_view_info(job_id, entry)
Scott Zawalski0acfe112012-03-06 09:21:44 -0500280
281 log_links.append(generate_log_link(entry['test_name'],
282 job_name))
Zdenek Behan150fbd62012-04-06 17:20:01 +0200283 if code == 1:
284 # Failed already, no need to worry further.
285 continue
286 if (entry['status'] == 'WARN' or
287 (is_fail_status(entry['status']) and experimental)):
288 # Failures that produce a warning. Either a test with WARN
289 # status or any experimental test failure.
Chris Masone5374c672012-03-05 15:11:39 -0800290 code = 2
291 else:
292 code = 1
Chris Masoneb61b4052012-04-30 14:35:28 -0700293 print timings
Chris Masone1120cdf2012-02-27 17:35:07 -0800294 for link in log_links:
295 print link
Chris Masone24b80f12012-02-14 14:18:01 -0800296 break
Chris Masoned5939fe2012-03-13 10:11:06 -0700297 else:
298 print "Created suite job: %r" % job_id
299 print generate_log_link(options.name,
300 '%s-%s' % (job_id, getpass.getuser()))
301 print "--no_wait specified; Exiting."
Chris Masone24b80f12012-02-14 14:18:01 -0800302 return code
303
304if __name__ == "__main__":
305 sys.exit(main())