blob: 5ae2aa4bfe9a78ce38d2666c92486dd4ee1e8dbb [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 Masone8ac66712012-02-15 14:21:02 -080021from autotest_lib.server.cros import 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
Zdenek Behan150fbd62012-04-06 17:20:01 +020025# This is the prefix that is used to denote an experimental test.
26EXPERIMENTAL_PREFIX = 'experimental_'
Chris Masone1120cdf2012-02-27 17:35:07 -080027
Chris Masonedfa0beba2012-03-19 11:41:47 -070028
29class RunSuiteLoggingConfig(logging_config.LoggingConfig):
30 def configure_logging(self, verbose=False):
31 super(RunSuiteLoggingConfig, self).configure_logging(use_console=True)
32
33
Chris Masone24b80f12012-02-14 14:18:01 -080034def parse_options():
35 usage = "usage: %prog [options] control_file"
36 parser = optparse.OptionParser(usage=usage)
37 parser.add_option("-b", "--board", dest="board")
38 parser.add_option("-i", "--build", dest="build")
Chris Masone359c0fd2012-03-13 15:18:59 -070039 # This should just be a boolean flag, but the autotest "proxy" code
40 # can't handle flags that don't take arguments.
41 parser.add_option("-n", "--no_wait", dest="no_wait", default=None)
Scott Zawalski65650172012-02-16 11:48:26 -050042 parser.add_option("-p", "--pool", dest="pool", default=None)
Chris Masone24b80f12012-02-14 14:18:01 -080043 parser.add_option("-s", "--suite_name", dest="name")
Chris Masone8ac66712012-02-15 14:21:02 -080044 parser.add_option("-t", "--timeout_min", dest="timeout_min", default=30)
45 parser.add_option("-d", "--delay_sec", dest="delay_sec", default=10)
Chris Masone986459e2012-04-11 11:36:48 -070046 parser.add_option("-m", "--mock_job_id", dest="mock_job_id",
47 help="Skips running suite; creates report for given ID.")
Chris Masone24b80f12012-02-14 14:18:01 -080048 options, args = parser.parse_args()
49 return parser, options, args
50
51
52def get_pretty_status(status):
53 if status == 'GOOD':
54 return '[ PASSED ]'
55 return '[ FAILED ]'
56
Zdenek Behan150fbd62012-04-06 17:20:01 +020057def is_fail_status(status):
58 # All the statuses tests can have when they fail.
59 if status in ['FAIL', 'ERROR', 'TEST_NA']:
60 return True
61 return False
62
Chris Masone24b80f12012-02-14 14:18:01 -080063
64def status_is_relevant(status):
65 """
66 Indicates whether the status of a given test is meaningful or not.
67
68 @param status: frontend.TestStatus object to look at.
69 @return True if this is a test result worth looking at further.
70 """
Chris Masone986459e2012-04-11 11:36:48 -070071 return not status['test_name'].startswith('CLIENT_JOB')
Chris Masone24b80f12012-02-14 14:18:01 -080072
73
Chris Masone1120cdf2012-02-27 17:35:07 -080074def generate_log_link(anchor, job_string):
75 """
76 Generate a link to this job's logs, for consumption by buildbot.
77
78 @param anchor: Link anchor text.
79 @param job_id: the job whose logs we'd like to link to.
80 @return A link formatted for the buildbot log annotator.
81 """
82 host = CONFIG.get_config_value('SERVER', 'hostname', type=str)
83 pattern = CONFIG.get_config_value('CROS', 'log_url_pattern', type=str)
84 return "@@@STEP_LINK@%s@%s@@@" % (anchor, pattern % (host, job_string))
85
86
Zdenek Behan150fbd62012-04-06 17:20:01 +020087def get_view_info(suite_job_id, view):
88 """
89 Parse a view for the slave job name and job_id.
90
91 @param suite_job_id: The job id of our master suite job.
92 @param view: Test result view.
93 @return A tuple job_name, experimental of the slave test run
94 described by view.
95 """
96 # By default, we are the main suite job since there is no
97 # keyval entry for our job_name.
98 job_name = '%s-%s' % (suite_job_id, getpass.getuser())
99 experimental = False
100 if 'job_keyvals' in view:
101 # The job name depends on whether it's experimental or not.
102 std_job_name = view['test_name'].split('.')[0]
103 exp_job_name = EXPERIMENTAL_PREFIX + std_job_name
104 if std_job_name in view['job_keyvals']:
105 job_name = view['job_keyvals'][std_job_name]
106 elif exp_job_name in view['job_keyvals']:
107 experimental = True
108 job_name = view['job_keyvals'][exp_job_name]
109 return job_name, experimental
110
111
Chris Masoneb61b4052012-04-30 14:35:28 -0700112class Timings(object):
113 """Timings for important events during a suite.
114
115 All timestamps are datetime.datetime objects.
116
117 @var suite_start_time: the time the suite started.
118 @var reimage_start_time: the time we started reimaging devices.
119 @var reimage_end_time: the time we finished reimaging devices.
120 @var tests_start_time: the time the first test started running.
121 """
Chris Masonea8066a92012-05-01 16:52:31 -0700122 download_start_time = None
123 payload_end_time = None
124 artifact_end_time = None
Chris Masoneb61b4052012-04-30 14:35:28 -0700125 suite_start_time = None
126 reimage_start_time = None
127 reimage_end_time = None
128 tests_start_time = None
129 tests_end_time = None
130
131
132 def RecordTiming(self, entry):
133 """Given a test report entry, extract and record pertinent time info.
134
135 get_detailed_test_views() returns a list of entries that provide
136 info about the various parts of a suite run. This method can take
137 any one of these entries and look up timestamp info we might want
138 and record it.
139
140 @param entry: an entry dict, as returned by get_details_test_views().
141 """
142 time_fmt = '%Y-%m-%d %H:%M:%S'
143 start_candidate = datetime.datetime.strptime(entry['test_started_time'],
144 time_fmt)
145 end_candidate = datetime.datetime.strptime(entry['test_finished_time'],
146 time_fmt)
147 if entry['test_name'] == 'SERVER_JOB':
148 self.suite_start_time = start_candidate
149 elif entry['test_name'] == 'try_new_image':
150 self.reimage_start_time = start_candidate
151 self.reimage_end_time = end_candidate
152 else:
153 self._UpdateFirstTestStartTime(start_candidate)
154 self._UpdateLastTestEndTime(end_candidate)
Chris Masonea8066a92012-05-01 16:52:31 -0700155 if 'job_keyvals' in entry:
156 keyvals = entry['job_keyvals']
157 self.download_start_time = keyvals.get('download_started_time')
158 self.payload_end_time = keyvals.get('payload_finished_time')
159 self.artifact_end_time = keyvals.get('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())