Chris Masone | 24b80f1 | 2012-02-14 14:18:01 -0800 | [diff] [blame] | 1 | #!/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 | |
| 9 | The desired test suite will be scheduled with autotest, and then |
| 10 | this tool will block until the job is complete, printing a summary |
| 11 | at the end. Error conditions result in exceptions. |
| 12 | |
| 13 | This is intended for use only with Chrome OS test suits that leverage the |
| 14 | dynamic suite infrastructure in server/cros/dynamic_suite.py. |
| 15 | """ |
| 16 | |
Chris Masone | 11aae45 | 2012-05-21 16:08:39 -0700 | [diff] [blame] | 17 | import datetime, getpass, hashlib, optparse, time, sys |
Chris Masone | 24b80f1 | 2012-02-14 14:18:01 -0800 | [diff] [blame] | 18 | import common |
Chris Masone | dfa0beba | 2012-03-19 11:41:47 -0700 | [diff] [blame] | 19 | import logging |
Chris Masone | 1120cdf | 2012-02-27 17:35:07 -0800 | [diff] [blame] | 20 | from autotest_lib.client.common_lib import global_config |
Chris Masone | aa10f8e | 2012-05-15 13:34:21 -0700 | [diff] [blame] | 21 | from autotest_lib.server.cros import dynamic_suite, frontend_wrappers |
Chris Masone | 8d6e641 | 2012-06-28 11:20:56 -0700 | [diff] [blame] | 22 | from autotest_lib.server.cros import job_status |
Chris Masone | dfa0beba | 2012-03-19 11:41:47 -0700 | [diff] [blame] | 23 | from autotest_lib.client.common_lib import logging_config, logging_manager |
Chris Masone | 24b80f1 | 2012-02-14 14:18:01 -0800 | [diff] [blame] | 24 | |
Chris Masone | 1120cdf | 2012-02-27 17:35:07 -0800 | [diff] [blame] | 25 | CONFIG = global_config.global_config |
| 26 | |
Chris Masone | dfa0beba | 2012-03-19 11:41:47 -0700 | [diff] [blame] | 27 | |
| 28 | class RunSuiteLoggingConfig(logging_config.LoggingConfig): |
| 29 | def configure_logging(self, verbose=False): |
| 30 | super(RunSuiteLoggingConfig, self).configure_logging(use_console=True) |
| 31 | |
| 32 | |
Chris Masone | 24b80f1 | 2012-02-14 14:18:01 -0800 | [diff] [blame] | 33 | def parse_options(): |
Zdenek Behan | 77290c3 | 2012-06-26 17:39:47 +0200 | [diff] [blame] | 34 | usage = "usage: %prog [options]" |
Chris Masone | 24b80f1 | 2012-02-14 14:18:01 -0800 | [diff] [blame] | 35 | parser = optparse.OptionParser(usage=usage) |
| 36 | parser.add_option("-b", "--board", dest="board") |
| 37 | parser.add_option("-i", "--build", dest="build") |
Chris Masone | 359c0fd | 2012-03-13 15:18:59 -0700 | [diff] [blame] | 38 | # 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 Zawalski | 6565017 | 2012-02-16 11:48:26 -0500 | [diff] [blame] | 41 | parser.add_option("-p", "--pool", dest="pool", default=None) |
Chris Masone | 24b80f1 | 2012-02-14 14:18:01 -0800 | [diff] [blame] | 42 | parser.add_option("-s", "--suite_name", dest="name") |
Chris Masone | 8ac6671 | 2012-02-15 14:21:02 -0800 | [diff] [blame] | 43 | parser.add_option("-t", "--timeout_min", dest="timeout_min", default=30) |
| 44 | parser.add_option("-d", "--delay_sec", dest="delay_sec", default=10) |
Chris Masone | 986459e | 2012-04-11 11:36:48 -0700 | [diff] [blame] | 45 | parser.add_option("-m", "--mock_job_id", dest="mock_job_id", |
| 46 | help="Skips running suite; creates report for given ID.") |
Chris Masone | 24b80f1 | 2012-02-14 14:18:01 -0800 | [diff] [blame] | 47 | options, args = parser.parse_args() |
| 48 | return parser, options, args |
| 49 | |
| 50 | |
| 51 | def get_pretty_status(status): |
| 52 | if status == 'GOOD': |
| 53 | return '[ PASSED ]' |
| 54 | return '[ FAILED ]' |
| 55 | |
Zdenek Behan | 150fbd6 | 2012-04-06 17:20:01 +0200 | [diff] [blame] | 56 | def 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 Masone | 24b80f1 | 2012-02-14 14:18:01 -0800 | [diff] [blame] | 62 | |
| 63 | def 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 Masone | 986459e | 2012-04-11 11:36:48 -0700 | [diff] [blame] | 70 | return not status['test_name'].startswith('CLIENT_JOB') |
Chris Masone | 24b80f1 | 2012-02-14 14:18:01 -0800 | [diff] [blame] | 71 | |
| 72 | |
Chris Masone | 1120cdf | 2012-02-27 17:35:07 -0800 | [diff] [blame] | 73 | def 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 Behan | 150fbd6 | 2012-04-06 17:20:01 +0200 | [diff] [blame] | 86 | def 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 Masone | aa10f8e | 2012-05-15 13:34:21 -0700 | [diff] [blame] | 102 | exp_job_name = dynamic_suite.EXPERIMENTAL_PREFIX + std_job_name |
Chris Masone | 11aae45 | 2012-05-21 16:08:39 -0700 | [diff] [blame] | 103 | |
| 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 Behan | 150fbd6 | 2012-04-06 17:20:01 +0200 | [diff] [blame] | 115 | 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 Masone | 11aae45 | 2012-05-21 16:08:39 -0700 | [diff] [blame] | 120 | |
Zdenek Behan | 150fbd6 | 2012-04-06 17:20:01 +0200 | [diff] [blame] | 121 | return job_name, experimental |
| 122 | |
| 123 | |
Chris Masone | b61b405 | 2012-04-30 14:35:28 -0700 | [diff] [blame] | 124 | class 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 Masone | a8066a9 | 2012-05-01 16:52:31 -0700 | [diff] [blame] | 134 | download_start_time = None |
| 135 | payload_end_time = None |
| 136 | artifact_end_time = None |
Chris Masone | b61b405 | 2012-04-30 14:35:28 -0700 | [diff] [blame] | 137 | suite_start_time = None |
Chris Masone | 604baf3 | 2012-06-28 08:45:30 -0700 | [diff] [blame^] | 138 | reimage_times = {} # {'hostname': (start_time, end_time)} |
Chris Masone | b61b405 | 2012-04-30 14:35:28 -0700 | [diff] [blame] | 139 | 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 Masone | b61b405 | 2012-04-30 14:35:28 -0700 | [diff] [blame] | 153 | start_candidate = datetime.datetime.strptime(entry['test_started_time'], |
Chris Masone | 8d6e641 | 2012-06-28 11:20:56 -0700 | [diff] [blame] | 154 | job_status.TIME_FMT) |
Chris Masone | b61b405 | 2012-04-30 14:35:28 -0700 | [diff] [blame] | 155 | end_candidate = datetime.datetime.strptime(entry['test_finished_time'], |
Chris Masone | 8d6e641 | 2012-06-28 11:20:56 -0700 | [diff] [blame] | 156 | job_status.TIME_FMT) |
Chris Masone | b61b405 | 2012-04-30 14:35:28 -0700 | [diff] [blame] | 157 | if entry['test_name'] == 'SERVER_JOB': |
| 158 | self.suite_start_time = start_candidate |
Chris Masone | 604baf3 | 2012-06-28 08:45:30 -0700 | [diff] [blame^] | 159 | 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 Masone | b61b405 | 2012-04-30 14:35:28 -0700 | [diff] [blame] | 162 | else: |
| 163 | self._UpdateFirstTestStartTime(start_candidate) |
| 164 | self._UpdateLastTestEndTime(end_candidate) |
Chris Masone | a8066a9 | 2012-05-01 16:52:31 -0700 | [diff] [blame] | 165 | if 'job_keyvals' in entry: |
| 166 | keyvals = entry['job_keyvals'] |
Chris Masone | aa10f8e | 2012-05-15 13:34:21 -0700 | [diff] [blame] | 167 | 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 Masone | b61b405 | 2012-04-30 14:35:28 -0700 | [diff] [blame] | 173 | |
| 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 Masone | 604baf3 | 2012-06-28 08:45:30 -0700 | [diff] [blame^] | 193 | 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 Masone | b61b405 | 2012-04-30 14:35:28 -0700 | [diff] [blame] | 198 | return ('\n' |
| 199 | 'Suite timings:\n' |
Chris Masone | a8066a9 | 2012-05-01 16:52:31 -0700 | [diff] [blame] | 200 | 'Downloads started at %s\n' |
| 201 | 'Payload downloads ended at %s\n' |
Chris Masone | b61b405 | 2012-04-30 14:35:28 -0700 | [diff] [blame] | 202 | 'Suite started at %s\n' |
Chris Masone | 604baf3 | 2012-06-28 08:45:30 -0700 | [diff] [blame^] | 203 | '%s' |
Chris Masone | a8066a9 | 2012-05-01 16:52:31 -0700 | [diff] [blame] | 204 | 'Artifact downloads ended (at latest) at %s\n' |
Chris Masone | b61b405 | 2012-04-30 14:35:28 -0700 | [diff] [blame] | 205 | 'Testing started at %s\n' |
Chris Masone | a8066a9 | 2012-05-01 16:52:31 -0700 | [diff] [blame] | 206 | 'Testing ended at %s\n' % (self.download_start_time, |
| 207 | self.payload_end_time, |
| 208 | self.suite_start_time, |
Chris Masone | 604baf3 | 2012-06-28 08:45:30 -0700 | [diff] [blame^] | 209 | reimaging_info, |
Chris Masone | a8066a9 | 2012-05-01 16:52:31 -0700 | [diff] [blame] | 210 | self.artifact_end_time, |
Chris Masone | b61b405 | 2012-04-30 14:35:28 -0700 | [diff] [blame] | 211 | self.tests_start_time, |
| 212 | self.tests_end_time)) |
| 213 | |
| 214 | |
Chris Masone | 24b80f1 | 2012-02-14 14:18:01 -0800 | [diff] [blame] | 215 | def main(): |
| 216 | parser, options, args = parse_options() |
Chris Masone | 986459e | 2012-04-11 11:36:48 -0700 | [diff] [blame] | 217 | if not options.mock_job_id: |
Zdenek Behan | 77290c3 | 2012-06-26 17:39:47 +0200 | [diff] [blame] | 218 | 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 Masone | 986459e | 2012-04-11 11:36:48 -0700 | [diff] [blame] | 232 | parser.print_help() |
| 233 | return |
Chris Masone | dfa0beba | 2012-03-19 11:41:47 -0700 | [diff] [blame] | 234 | |
| 235 | logging_manager.configure_logging(RunSuiteLoggingConfig()) |
| 236 | |
Chris Masone | 8ac6671 | 2012-02-15 14:21:02 -0800 | [diff] [blame] | 237 | afe = frontend_wrappers.RetryingAFE(timeout_min=options.timeout_min, |
| 238 | delay_sec=options.delay_sec) |
Chris Masone | 359c0fd | 2012-03-13 15:18:59 -0700 | [diff] [blame] | 239 | |
| 240 | wait = options.no_wait is None |
Chris Masone | 986459e | 2012-04-11 11:36:48 -0700 | [diff] [blame] | 241 | 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 Masone | 8ac6671 | 2012-02-15 14:21:02 -0800 | [diff] [blame] | 250 | TKO = frontend_wrappers.RetryingTKO(timeout_min=options.timeout_min, |
| 251 | delay_sec=options.delay_sec) |
Chris Masone | 24b80f1 | 2012-02-14 14:18:01 -0800 | [diff] [blame] | 252 | # Return code that will be sent back to autotest_rpc_server.py |
Zdenek Behan | 150fbd6 | 2012-04-06 17:20:01 +0200 | [diff] [blame] | 253 | # 0 = OK |
| 254 | # 1 = ERROR |
| 255 | # 2 = WARNING |
Chris Masone | 24b80f1 | 2012-02-14 14:18:01 -0800 | [diff] [blame] | 256 | code = 0 |
Chris Masone | 359c0fd | 2012-03-13 15:18:59 -0700 | [diff] [blame] | 257 | while wait and True: |
Chris Masone | 24b80f1 | 2012-02-14 14:18:01 -0800 | [diff] [blame] | 258 | if not afe.get_jobs(id=job_id, finished=True): |
| 259 | time.sleep(1) |
| 260 | continue |
Scott Zawalski | 0acfe11 | 2012-03-06 09:21:44 -0500 | [diff] [blame] | 261 | views = TKO.run('get_detailed_test_views', afe_job_id=job_id) |
Chris Masone | 24b80f1 | 2012-02-14 14:18:01 -0800 | [diff] [blame] | 262 | width = len(max(map(lambda x: x['test_name'], views), key=len)) + 3 |
Scott Zawalski | 0acfe11 | 2012-03-06 09:21:44 -0500 | [diff] [blame] | 263 | |
| 264 | relevant_views = filter(status_is_relevant, views) |
| 265 | if not relevant_views: |
Zdenek Behan | 150fbd6 | 2012-04-06 17:20:01 +0200 | [diff] [blame] | 266 | # The main suite job most likely failed in SERVER_JOB. |
| 267 | relevant_views = views |
Scott Zawalski | 0acfe11 | 2012-03-06 09:21:44 -0500 | [diff] [blame] | 268 | |
Chris Masone | b61b405 | 2012-04-30 14:35:28 -0700 | [diff] [blame] | 269 | timings = Timings() |
Chris Masone | 1120cdf | 2012-02-27 17:35:07 -0800 | [diff] [blame] | 270 | log_links = [] |
Scott Zawalski | 0acfe11 | 2012-03-06 09:21:44 -0500 | [diff] [blame] | 271 | for entry in relevant_views: |
Chris Masone | b61b405 | 2012-04-30 14:35:28 -0700 | [diff] [blame] | 272 | timings.RecordTiming(entry) |
Chris Masone | 986459e | 2012-04-11 11:36:48 -0700 | [diff] [blame] | 273 | entry['test_name'] = entry['test_name'].replace('SERVER_JOB', |
| 274 | 'Suite prep') |
Chris Masone | 24b80f1 | 2012-02-14 14:18:01 -0800 | [diff] [blame] | 275 | 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 Behan | 150fbd6 | 2012-04-06 17:20:01 +0200 | [diff] [blame] | 281 | job_name, experimental = get_view_info(job_id, entry) |
Scott Zawalski | 0acfe11 | 2012-03-06 09:21:44 -0500 | [diff] [blame] | 282 | |
| 283 | log_links.append(generate_log_link(entry['test_name'], |
| 284 | job_name)) |
Zdenek Behan | 150fbd6 | 2012-04-06 17:20:01 +0200 | [diff] [blame] | 285 | 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 Masone | 5374c67 | 2012-03-05 15:11:39 -0800 | [diff] [blame] | 292 | code = 2 |
| 293 | else: |
| 294 | code = 1 |
Chris Masone | b61b405 | 2012-04-30 14:35:28 -0700 | [diff] [blame] | 295 | print timings |
Chris Masone | 1120cdf | 2012-02-27 17:35:07 -0800 | [diff] [blame] | 296 | for link in log_links: |
| 297 | print link |
Chris Masone | 24b80f1 | 2012-02-14 14:18:01 -0800 | [diff] [blame] | 298 | break |
Chris Masone | d5939fe | 2012-03-13 10:11:06 -0700 | [diff] [blame] | 299 | 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 Masone | 24b80f1 | 2012-02-14 14:18:01 -0800 | [diff] [blame] | 304 | return code |
| 305 | |
| 306 | if __name__ == "__main__": |
| 307 | sys.exit(main()) |