blob: f0aeee932cefa478088aef85650d6126937b4ae4 [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 Masoneb61b4052012-04-30 14:35:28 -070017import datetime, getpass, 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 Masonedfa0beba2012-03-19 11:41:47 -070022from autotest_lib.client.common_lib import logging_config, logging_manager
Chris Masone24b80f12012-02-14 14:18:01 -080023
Chris Masone1120cdf2012-02-27 17:35:07 -080024CONFIG = global_config.global_config
25
Chris Masonedfa0beba2012-03-19 11:41:47 -070026
27class RunSuiteLoggingConfig(logging_config.LoggingConfig):
28 def configure_logging(self, verbose=False):
29 super(RunSuiteLoggingConfig, self).configure_logging(use_console=True)
30
31
Chris Masone24b80f12012-02-14 14:18:01 -080032def parse_options():
33 usage = "usage: %prog [options] control_file"
34 parser = optparse.OptionParser(usage=usage)
35 parser.add_option("-b", "--board", dest="board")
36 parser.add_option("-i", "--build", dest="build")
Chris Masone359c0fd2012-03-13 15:18:59 -070037 # This should just be a boolean flag, but the autotest "proxy" code
38 # can't handle flags that don't take arguments.
39 parser.add_option("-n", "--no_wait", dest="no_wait", default=None)
Scott Zawalski65650172012-02-16 11:48:26 -050040 parser.add_option("-p", "--pool", dest="pool", default=None)
Chris Masone24b80f12012-02-14 14:18:01 -080041 parser.add_option("-s", "--suite_name", dest="name")
Chris Masone8ac66712012-02-15 14:21:02 -080042 parser.add_option("-t", "--timeout_min", dest="timeout_min", default=30)
43 parser.add_option("-d", "--delay_sec", dest="delay_sec", default=10)
Chris Masone986459e2012-04-11 11:36:48 -070044 parser.add_option("-m", "--mock_job_id", dest="mock_job_id",
45 help="Skips running suite; creates report for given ID.")
Chris Masone24b80f12012-02-14 14:18:01 -080046 options, args = parser.parse_args()
47 return parser, options, args
48
49
50def get_pretty_status(status):
51 if status == 'GOOD':
52 return '[ PASSED ]'
53 return '[ FAILED ]'
54
Zdenek Behan150fbd62012-04-06 17:20:01 +020055def is_fail_status(status):
56 # All the statuses tests can have when they fail.
57 if status in ['FAIL', 'ERROR', 'TEST_NA']:
58 return True
59 return False
60
Chris Masone24b80f12012-02-14 14:18:01 -080061
62def status_is_relevant(status):
63 """
64 Indicates whether the status of a given test is meaningful or not.
65
66 @param status: frontend.TestStatus object to look at.
67 @return True if this is a test result worth looking at further.
68 """
Chris Masone986459e2012-04-11 11:36:48 -070069 return not status['test_name'].startswith('CLIENT_JOB')
Chris Masone24b80f12012-02-14 14:18:01 -080070
71
Chris Masone1120cdf2012-02-27 17:35:07 -080072def generate_log_link(anchor, job_string):
73 """
74 Generate a link to this job's logs, for consumption by buildbot.
75
76 @param anchor: Link anchor text.
77 @param job_id: the job whose logs we'd like to link to.
78 @return A link formatted for the buildbot log annotator.
79 """
80 host = CONFIG.get_config_value('SERVER', 'hostname', type=str)
81 pattern = CONFIG.get_config_value('CROS', 'log_url_pattern', type=str)
82 return "@@@STEP_LINK@%s@%s@@@" % (anchor, pattern % (host, job_string))
83
84
Zdenek Behan150fbd62012-04-06 17:20:01 +020085def get_view_info(suite_job_id, view):
86 """
87 Parse a view for the slave job name and job_id.
88
89 @param suite_job_id: The job id of our master suite job.
90 @param view: Test result view.
91 @return A tuple job_name, experimental of the slave test run
92 described by view.
93 """
94 # By default, we are the main suite job since there is no
95 # keyval entry for our job_name.
96 job_name = '%s-%s' % (suite_job_id, getpass.getuser())
97 experimental = False
98 if 'job_keyvals' in view:
99 # The job name depends on whether it's experimental or not.
100 std_job_name = view['test_name'].split('.')[0]
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700101 exp_job_name = dynamic_suite.EXPERIMENTAL_PREFIX + std_job_name
Zdenek Behan150fbd62012-04-06 17:20:01 +0200102 if std_job_name in view['job_keyvals']:
103 job_name = view['job_keyvals'][std_job_name]
104 elif exp_job_name in view['job_keyvals']:
105 experimental = True
106 job_name = view['job_keyvals'][exp_job_name]
107 return job_name, experimental
108
109
Chris Masoneb61b4052012-04-30 14:35:28 -0700110class Timings(object):
111 """Timings for important events during a suite.
112
113 All timestamps are datetime.datetime objects.
114
115 @var suite_start_time: the time the suite started.
116 @var reimage_start_time: the time we started reimaging devices.
117 @var reimage_end_time: the time we finished reimaging devices.
118 @var tests_start_time: the time the first test started running.
119 """
Chris Masonea8066a92012-05-01 16:52:31 -0700120 download_start_time = None
121 payload_end_time = None
122 artifact_end_time = None
Chris Masoneb61b4052012-04-30 14:35:28 -0700123 suite_start_time = None
124 reimage_start_time = None
125 reimage_end_time = None
126 tests_start_time = None
127 tests_end_time = None
128
129
130 def RecordTiming(self, entry):
131 """Given a test report entry, extract and record pertinent time info.
132
133 get_detailed_test_views() returns a list of entries that provide
134 info about the various parts of a suite run. This method can take
135 any one of these entries and look up timestamp info we might want
136 and record it.
137
138 @param entry: an entry dict, as returned by get_details_test_views().
139 """
Chris Masoneb61b4052012-04-30 14:35:28 -0700140 start_candidate = datetime.datetime.strptime(entry['test_started_time'],
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700141 dynamic_suite.TIME_FMT)
Chris Masoneb61b4052012-04-30 14:35:28 -0700142 end_candidate = datetime.datetime.strptime(entry['test_finished_time'],
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700143 dynamic_suite.TIME_FMT)
Chris Masoneb61b4052012-04-30 14:35:28 -0700144 if entry['test_name'] == 'SERVER_JOB':
145 self.suite_start_time = start_candidate
146 elif entry['test_name'] == 'try_new_image':
147 self.reimage_start_time = start_candidate
148 self.reimage_end_time = end_candidate
149 else:
150 self._UpdateFirstTestStartTime(start_candidate)
151 self._UpdateLastTestEndTime(end_candidate)
Chris Masonea8066a92012-05-01 16:52:31 -0700152 if 'job_keyvals' in entry:
153 keyvals = entry['job_keyvals']
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700154 self.download_start_time = keyvals.get(
155 dynamic_suite.DOWNLOAD_STARTED_TIME)
156 self.payload_end_time = keyvals.get(
157 dynamic_suite.PAYLOAD_FINISHED_TIME)
158 self.artifact_end_time = keyvals.get(
159 dynamic_suite.ARTIFACT_FINISHED_TIME)
Chris Masoneb61b4052012-04-30 14:35:28 -0700160
161 def _UpdateFirstTestStartTime(self, candidate):
162 """Update self.tests_start_time, iff candidate is an earlier time.
163
164 @param candidate: a datetime.datetime object.
165 """
166 if not self.tests_start_time or candidate < self.tests_start_time:
167 self.tests_start_time = candidate
168
169
170 def _UpdateLastTestEndTime(self, candidate):
171 """Update self.tests_end_time, iff candidate is a later time.
172
173 @param candidate: a datetime.datetime object.
174 """
175 if not self.tests_end_time or candidate > self.tests_end_time:
176 self.tests_end_time = candidate
177
178
179 def __str__(self):
180 return ('\n'
181 'Suite timings:\n'
Chris Masonea8066a92012-05-01 16:52:31 -0700182 'Downloads started at %s\n'
183 'Payload downloads ended at %s\n'
Chris Masoneb61b4052012-04-30 14:35:28 -0700184 'Suite started at %s\n'
185 'Reimaging started at %s\n'
186 'Reimaging ended at %s\n'
Chris Masonea8066a92012-05-01 16:52:31 -0700187 'Artifact downloads ended (at latest) at %s\n'
Chris Masoneb61b4052012-04-30 14:35:28 -0700188 'Testing started at %s\n'
Chris Masonea8066a92012-05-01 16:52:31 -0700189 'Testing ended at %s\n' % (self.download_start_time,
190 self.payload_end_time,
191 self.suite_start_time,
Chris Masoneb61b4052012-04-30 14:35:28 -0700192 self.reimage_start_time,
193 self.reimage_end_time,
Chris Masonea8066a92012-05-01 16:52:31 -0700194 self.artifact_end_time,
Chris Masoneb61b4052012-04-30 14:35:28 -0700195 self.tests_start_time,
196 self.tests_end_time))
197
198
Chris Masone24b80f12012-02-14 14:18:01 -0800199def main():
200 parser, options, args = parse_options()
Chris Masone986459e2012-04-11 11:36:48 -0700201 if not options.mock_job_id:
202 if args or not options.build or not options.board or not options.name:
203 parser.print_help()
204 return
Chris Masonedfa0beba2012-03-19 11:41:47 -0700205
206 logging_manager.configure_logging(RunSuiteLoggingConfig())
207
Chris Masone8ac66712012-02-15 14:21:02 -0800208 afe = frontend_wrappers.RetryingAFE(timeout_min=options.timeout_min,
209 delay_sec=options.delay_sec)
Chris Masone359c0fd2012-03-13 15:18:59 -0700210
211 wait = options.no_wait is None
Chris Masone986459e2012-04-11 11:36:48 -0700212 if options.mock_job_id:
213 job_id = int(options.mock_job_id)
214 else:
215 job_id = afe.run('create_suite_job',
216 suite_name=options.name,
217 board=options.board,
218 build=options.build,
219 check_hosts=wait,
220 pool=options.pool)
Chris Masone8ac66712012-02-15 14:21:02 -0800221 TKO = frontend_wrappers.RetryingTKO(timeout_min=options.timeout_min,
222 delay_sec=options.delay_sec)
Chris Masone24b80f12012-02-14 14:18:01 -0800223 # Return code that will be sent back to autotest_rpc_server.py
Zdenek Behan150fbd62012-04-06 17:20:01 +0200224 # 0 = OK
225 # 1 = ERROR
226 # 2 = WARNING
Chris Masone24b80f12012-02-14 14:18:01 -0800227 code = 0
Chris Masone359c0fd2012-03-13 15:18:59 -0700228 while wait and True:
Chris Masone24b80f12012-02-14 14:18:01 -0800229 if not afe.get_jobs(id=job_id, finished=True):
230 time.sleep(1)
231 continue
Scott Zawalski0acfe112012-03-06 09:21:44 -0500232 views = TKO.run('get_detailed_test_views', afe_job_id=job_id)
Chris Masone24b80f12012-02-14 14:18:01 -0800233 width = len(max(map(lambda x: x['test_name'], views), key=len)) + 3
Scott Zawalski0acfe112012-03-06 09:21:44 -0500234
235 relevant_views = filter(status_is_relevant, views)
236 if not relevant_views:
Zdenek Behan150fbd62012-04-06 17:20:01 +0200237 # The main suite job most likely failed in SERVER_JOB.
238 relevant_views = views
Scott Zawalski0acfe112012-03-06 09:21:44 -0500239
Chris Masoneb61b4052012-04-30 14:35:28 -0700240 timings = Timings()
Chris Masone1120cdf2012-02-27 17:35:07 -0800241 log_links = []
Scott Zawalski0acfe112012-03-06 09:21:44 -0500242 for entry in relevant_views:
Chris Masoneb61b4052012-04-30 14:35:28 -0700243 timings.RecordTiming(entry)
Chris Masone986459e2012-04-11 11:36:48 -0700244 entry['test_name'] = entry['test_name'].replace('SERVER_JOB',
245 'Suite prep')
Chris Masone24b80f12012-02-14 14:18:01 -0800246 test_entry = entry['test_name'].ljust(width)
247 print "%s%s" % (test_entry, get_pretty_status(entry['status']))
248 if entry['status'] != 'GOOD':
249 print "%s %s: %s" % (test_entry,
250 entry['status'],
251 entry['reason'])
Zdenek Behan150fbd62012-04-06 17:20:01 +0200252 job_name, experimental = get_view_info(job_id, entry)
Scott Zawalski0acfe112012-03-06 09:21:44 -0500253
254 log_links.append(generate_log_link(entry['test_name'],
255 job_name))
Zdenek Behan150fbd62012-04-06 17:20:01 +0200256 if code == 1:
257 # Failed already, no need to worry further.
258 continue
259 if (entry['status'] == 'WARN' or
260 (is_fail_status(entry['status']) and experimental)):
261 # Failures that produce a warning. Either a test with WARN
262 # status or any experimental test failure.
Chris Masone5374c672012-03-05 15:11:39 -0800263 code = 2
264 else:
265 code = 1
Chris Masoneb61b4052012-04-30 14:35:28 -0700266 print timings
Chris Masone1120cdf2012-02-27 17:35:07 -0800267 for link in log_links:
268 print link
Chris Masone24b80f12012-02-14 14:18:01 -0800269 break
Chris Masoned5939fe2012-03-13 10:11:06 -0700270 else:
271 print "Created suite job: %r" % job_id
272 print generate_log_link(options.name,
273 '%s-%s' % (job_id, getpass.getuser()))
274 print "--no_wait specified; Exiting."
Chris Masone24b80f12012-02-14 14:18:01 -0800275 return code
276
277if __name__ == "__main__":
278 sys.exit(main())