blob: 13e5ce8b25064f5336e7d0e677cdd8ca6be978fe [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
Fang Deng5a43be62014-05-07 17:17:04 -07007
Chris Masone24b80f12012-02-14 14:18:01 -08008"""Tool for running suites of tests and waiting for completion.
9
Fang Deng5a43be62014-05-07 17:17:04 -070010The desired test suite will be scheduled with autotest. By default,
Chris Masone24b80f12012-02-14 14:18:01 -080011this tool will block until the job is complete, printing a summary
12at the end. Error conditions result in exceptions.
13
14This is intended for use only with Chrome OS test suits that leverage the
15dynamic suite infrastructure in server/cros/dynamic_suite.py.
Fang Deng5a43be62014-05-07 17:17:04 -070016
17This script exits with one of the following codes:
180 - OK: Suite finished successfully
191 - ERROR: Test(s) failed, or hits its own timeout
Fang Dengaeab6172014-05-07 17:17:04 -0700202 - WARNING: Test(s) raised a warning or passed on retry, none failed/timed out.
Fang Deng5a43be62014-05-07 17:17:04 -0700213 - INFRA_FAILURE: Infrastructure related issues, e.g.
22 * Lab is down
23 * Too many duts (defined as a constant) in repair failed status
24 * Suite job issues, like bug in dynamic suite,
25 user aborted the suite, lose a drone/all devservers/rpc server,
26 0 tests ran, etc.
Fang Deng95af42f2014-09-12 14:16:11 -070027 * provision failed
28 TODO(fdeng): crbug.com/413918, reexamine treating all provision
29 failures as INFRA failures.
Fang Deng5a43be62014-05-07 17:17:04 -0700304 - SUITE_TIMEOUT: Suite timed out, some tests ran,
31 none failed by the time the suite job was aborted. This will cover,
32 but not limited to, the following cases:
33 * A devserver failure that manifests as a timeout
34 * No DUTs available midway through a suite
35 * Provision/Reset/Cleanup took longer time than expected for new image
36 * A regression in scheduler tick time.
Fang Deng6197da32014-09-25 10:18:48 -0700375- BOARD_NOT_AVAILABLE: If there is no host for the requested board/pool.
386- INVALID_OPTIONS: If options are not valid.
Chris Masone24b80f12012-02-14 14:18:01 -080039"""
40
Allen Li93f4db52016-09-14 14:44:59 -070041import argparse
42import ast
Allen Licc205492017-07-10 17:26:04 -070043import collections
Allen Li340414e2016-08-16 14:19:08 -070044from collections import namedtuple
Chris Masonecfa7efc2012-09-06 16:00:07 -070045from datetime import datetime
Allen Li93f4db52016-09-14 14:44:59 -070046from datetime import timedelta
Allen Licc205492017-07-10 17:26:04 -070047import functools
Allen Li93f4db52016-09-14 14:44:59 -070048import getpass
49import json
50import logging
51import os
52import re
53import sys
54import time
Allen Li04afc8f2017-11-27 15:36:34 -080055import warnings
Chris Masonecfa7efc2012-09-06 16:00:07 -070056
Chris Masone24b80f12012-02-14 14:18:01 -080057import common
Allen Lie082ced2016-09-14 15:19:20 -070058from chromite.lib import buildbot_annotations as annotations
59
Shuqian Zhao2fecacd2015-08-05 22:56:30 -070060from autotest_lib.client.common_lib import control_data
Fang Deng5a43be62014-05-07 17:17:04 -070061from autotest_lib.client.common_lib import error
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080062from autotest_lib.client.common_lib import global_config, enum
63from autotest_lib.client.common_lib import priorities
Dan Shidfea3682014-08-10 23:38:40 -070064from autotest_lib.client.common_lib import time_utils
Prashanth B6285f6a2014-05-08 18:01:27 -070065from autotest_lib.client.common_lib.cros import retry
Prathmesh Prabhucd246f52018-01-03 13:45:48 -080066from autotest_lib.frontend.afe import rpc_client_lib
Prashanth B923ca262014-03-14 12:36:29 -070067from autotest_lib.frontend.afe.json_rpc import proxy
xixuanae791b12017-06-29 15:40:19 -070068from autotest_lib.server import site_utils
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080069from autotest_lib.server import utils
Dan Shi36cfd832014-10-10 13:38:51 -070070from autotest_lib.server.cros import provision
Chris Masone44e4d6c2012-08-15 14:25:53 -070071from autotest_lib.server.cros.dynamic_suite import constants
Chris Masoneb4935552012-08-14 12:05:54 -070072from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
Prashanth B923ca262014-03-14 12:36:29 -070073from autotest_lib.server.cros.dynamic_suite import reporting_utils
J. Richard Barnettee7b98bb2013-08-21 16:34:16 -070074from autotest_lib.server.cros.dynamic_suite import tools
Prashanth B923ca262014-03-14 12:36:29 -070075from autotest_lib.site_utils import diagnosis_utils
MK Ryu977a9752014-10-21 11:58:09 -070076from autotest_lib.site_utils import job_overhead
77
Chris Masone1120cdf2012-02-27 17:35:07 -080078CONFIG = global_config.global_config
79
Allen Lidc2c69a2016-09-14 19:05:47 -070080_DEFAULT_AUTOTEST_INSTANCE = CONFIG.get_config_value(
81 'SERVER', 'hostname', type=str)
82_URL_PATTERN = CONFIG.get_config_value('CROS', 'log_url_pattern', type=str)
Simran Basi7203d4e2015-02-03 15:50:18 -080083
Simran Basi22aa9fe2012-12-07 16:37:09 -080084# Return code that will be sent back to autotest_rpc_server.py
Fang Deng5a43be62014-05-07 17:17:04 -070085RETURN_CODES = enum.Enum(
Fang Dengfb4a9492014-09-18 17:52:06 -070086 'OK', 'ERROR', 'WARNING', 'INFRA_FAILURE', 'SUITE_TIMEOUT',
Simran Basiba90ec82017-02-17 02:02:50 +000087 'BOARD_NOT_AVAILABLE', 'INVALID_OPTIONS')
Fang Deng5a43be62014-05-07 17:17:04 -070088
xixuanae791b12017-06-29 15:40:19 -070089# Minimum RPC timeout setting for calls expected to take long time, e.g.,
90# create_suite_job. If default socket time (socket.getdefaulttimeout()) is
91# None or greater than this value, the default will be used.
92# The value here is set to be the same as the timeout for the RetryingAFE object
93# so long running RPCs can wait long enough before being aborted.
94_MIN_RPC_TIMEOUT = 600
95
96# Number of days back to search for existing job.
97_SEARCH_JOB_MAX_DAYS = 14
98
Allen Li977760b2017-11-06 18:11:37 -080099_PROVISION_SUITE = 'provision'
100
Fang Deng5a43be62014-05-07 17:17:04 -0700101
Allen Licc205492017-07-10 17:26:04 -0700102@functools.total_ordering
103class _ReturnResult(object):
104 """Represents overall result of run_suite operation.
Fang Deng5a43be62014-05-07 17:17:04 -0700105
Allen Licc205492017-07-10 17:26:04 -0700106 _ReturnResult instances sort based on priority (the order in
107 _RETURN_RESULTS).
Fang Deng5a43be62014-05-07 17:17:04 -0700108
Allen Licc205492017-07-10 17:26:04 -0700109 Furthermore, _ReturnResult instances can be combined by bitwise or
110 ("union"), which returns the instance with the higher priority
111 between the two (the instance with higher priority is a "superset"
112 of the other).
Fang Deng5a43be62014-05-07 17:17:04 -0700113
Allen Licc205492017-07-10 17:26:04 -0700114 Do not create new instances of this; use _RETURN_RESULTS instead.
Fang Deng5a43be62014-05-07 17:17:04 -0700115 """
Allen Licc205492017-07-10 17:26:04 -0700116
117 def __init__(self, return_code, message):
118 self.return_code = return_code
119 self.message = message
120
121 def __repr__(self):
122 return '<{cls} {key}, {this.return_code}, {this.message}>'.format(
123 cls=type(self).__name__,
124 key=self._getkey(),
125 this=self)
126
127 def __gt__(self, other):
128 if isinstance(other, type(self)):
129 return self._getkey() > other._getkey()
130 else:
131 return NotImplemented
132
133 def __eq__(self, other):
134 if isinstance(other, type(self)):
135 return (self.return_code == other.return_code
136 and self.message == other.message)
137 else:
138 return NotImplemented
139
140 def __hash__(self):
141 return hash(self.return_code) ^ hash(self.message)
142
143 def __or__(self, other):
144 if isinstance(other, type(self)):
145 if self > other:
146 return self
147 else:
148 return other
149 else:
150 return NotImplemented
151
152 def _getkey(self):
153 """Return sort key."""
154 return _RETURN_RESULTS_LIST.index(self)
155
156 def suite_result(self, output_dict=None):
157 """Make a SuiteResult using this _ReturnResult.
158
159 @param output_dict: output_dict to merge into SuiteResult.
160 """
161 if output_dict is None:
162 output_dict = dict()
163 else:
164 output_dict = output_dict.copy()
165 if self.message:
166 output_dict['return_message'] = self.message
167 return SuiteResult(self.return_code, output_dict)
168
169
170_RETURN_RESULTS = collections.OrderedDict([
171 ('ok', _ReturnResult(RETURN_CODES.OK, '')),
172
173 ('test_warning', _ReturnResult(
174 RETURN_CODES.WARNING, 'Test job raised warning.')),
175 ('suite_warning', _ReturnResult(
176 RETURN_CODES.WARNING, 'Suite job raised warning.')),
177 ('test_retry', _ReturnResult(
178 RETURN_CODES.WARNING, 'Tests were retried.')),
179
Prathmesh Prabhu316180c2017-12-19 16:06:44 -0800180 ('test_aborted_prestart', _ReturnResult(
Allen Licc205492017-07-10 17:26:04 -0700181 RETURN_CODES.SUITE_TIMEOUT,
182 'Tests were aborted before running; suite must have timed out.')),
Prathmesh Prabhu316180c2017-12-19 16:06:44 -0800183 # This really indicates a user action or an infra failure. But, suite
184 # timeouts cause similar fauilres in the individual tests, so we must
185 # classify these lower than suite_timeout. In case of a suite_timeout, the
186 # result from the suite job will promote the result to suite_timeout.
187 ('test_aborted_mystery',
188 _ReturnResult(
189 RETURN_CODES.SUITE_TIMEOUT,
190 'Tests were aborted after running, but before timeout; '
191 'Test was manually aborted or parsing results failed: '
192 'crbug.com/796348.')),
Allen Licc205492017-07-10 17:26:04 -0700193 ('suite_timeout', _ReturnResult(
194 RETURN_CODES.SUITE_TIMEOUT, 'Suite job timed out.')),
195
196 ('test_views_missing', _ReturnResult(
197 RETURN_CODES.INFRA_FAILURE, 'No test views found.')),
198 ('suite_failed', _ReturnResult(
199 RETURN_CODES.INFRA_FAILURE, 'Suite job failed.')),
200 ('provision_failed', _ReturnResult(
201 RETURN_CODES.INFRA_FAILURE, 'Provisioning failed.')),
202
203 ('test_failure', _ReturnResult(
204 RETURN_CODES.ERROR, 'Tests failed.')),
205])
206_RETURN_RESULTS_LIST = list(_RETURN_RESULTS.values())
Simran Basi22aa9fe2012-12-07 16:37:09 -0800207
Chris Masonedfa0beba2012-03-19 11:41:47 -0700208
Allen Li93f4db52016-09-14 14:44:59 -0700209def bool_str(x):
210 """Boolean string type for option arguments.
211
212 @param x: string representation of boolean value.
213
214 """
215 if x == 'True':
216 return True
217 elif x == 'False':
218 return False
219 else:
220 raise argparse.ArgumentTypeError(
221 '%s is not one of True or False' % (x,))
222
223
Allen Li603728a2016-12-08 13:58:11 -0800224def _get_priority_value(x):
225 """Convert a priority representation to its int value.
226
227 Priorities can be described either by an int value (possibly as a string)
228 or a name string. This function coerces both forms to an int value.
229
230 This function is intended for casting command line arguments during
231 parsing.
232
233 @param x: priority value as an int, int string, or name string
234
235 @returns: int value of priority
236 """
237 try:
238 return int(x)
239 except ValueError:
240 try:
241 return priorities.Priority.get_value(x)
242 except AttributeError:
243 raise argparse.ArgumentTypeError(
244 'Unknown priority level %s. Try one of %s.'
245 % (x, ', '.join(priorities.Priority.names)))
246
247
Allen Li93f4db52016-09-14 14:44:59 -0700248def make_parser():
249 """Make ArgumentParser instance for run_suite.py."""
250 parser = argparse.ArgumentParser(
251 usage="%(prog)s [options]")
252 parser.add_argument("-b", "--board", dest="board")
Prathmesh Prabhu9b8e7ad2017-10-30 14:26:13 -0700253 parser.add_argument(
254 "--model",
255 help="The device model to run tests against. For non-unified "
256 "builds, model and board are synonymous, but board is more "
257 "accurate in some cases. Only pass this option if your build "
258 "is a unified build.",
259 )
Allen Li93f4db52016-09-14 14:44:59 -0700260 parser.add_argument("-i", "--build", dest="build")
261 parser.add_argument(
262 "-w", "--web", dest="web", default=None,
263 help="Address of a webserver to receive suite requests.")
264 parser.add_argument(
Rohit Makasanadf0a3a32017-06-30 13:55:18 -0700265 '--cheets_build', dest='cheets_build', default=None,
266 help='ChromeOS Android build to be installed on dut.')
267 parser.add_argument(
Allen Li93f4db52016-09-14 14:44:59 -0700268 '--firmware_rw_build', dest='firmware_rw_build', default=None,
269 help='Firmware build to be installed in dut RW firmware.')
270 parser.add_argument(
271 '--firmware_ro_build', dest='firmware_ro_build', default=None,
272 help='Firmware build to be installed in dut RO firmware.')
273 parser.add_argument(
274 '--test_source_build', dest='test_source_build', default=None,
275 help=('Build that contains the test code, '
276 'e.g., it can be the value of `--build`, '
277 '`--firmware_rw_build` or `--firmware_ro_build` '
278 'arguments. Default is None, that is, use the test '
279 'code from `--build` (CrOS image)'))
Chris Masone359c0fd2012-03-13 15:18:59 -0700280 # This should just be a boolean flag, but the autotest "proxy" code
281 # can't handle flags that don't take arguments.
Allen Li93f4db52016-09-14 14:44:59 -0700282 parser.add_argument(
283 "-n", "--no_wait", dest="no_wait", default=False, type=bool_str,
284 help='Must pass "True" or "False" if used.')
Alex Miller0032e932013-10-23 12:52:58 -0700285 # If you really want no pool, --pool="" will do it. USE WITH CARE.
Allen Li93f4db52016-09-14 14:44:59 -0700286 parser.add_argument("-p", "--pool", dest="pool", default="suites")
287 parser.add_argument("-s", "--suite_name", dest="name")
288 parser.add_argument("-a", "--afe_timeout_mins", type=int,
289 dest="afe_timeout_mins", default=30)
290 parser.add_argument("-t", "--timeout_mins", type=int,
291 dest="timeout_mins", default=1440)
292 parser.add_argument("-x", "--max_runtime_mins", type=int,
293 dest="max_runtime_mins", default=1440)
294 parser.add_argument("-d", "--delay_sec", type=int,
295 dest="delay_sec", default=10)
296 parser.add_argument("-m", "--mock_job_id", dest="mock_job_id",
297 help="Attach to existing job id for already running "
298 "suite, and creates report.")
Aviv Keshetdb321de2015-04-10 19:09:58 -0700299 # NOTE(akeshet): This looks similar to --no_wait, but behaves differently.
300 # --no_wait is passed in to the suite rpc itself and affects the suite,
301 # while this does not.
Allen Li93f4db52016-09-14 14:44:59 -0700302 parser.add_argument("-c", "--create_and_return", dest="create_and_return",
303 action="store_true",
304 help="Create the suite and print the job id, then "
305 "finish immediately.")
306 parser.add_argument("-u", "--num", dest="num", type=int, default=None,
Allen Li04afc8f2017-11-27 15:36:34 -0800307 help="Deprecated, does nothing.")
Alex Millerf43d0eb2012-10-01 13:43:13 -0700308 # Same boolean flag issue applies here.
Allen Li93f4db52016-09-14 14:44:59 -0700309 parser.add_argument(
310 "-f", "--file_bugs", dest="file_bugs", default=False, type=bool_str,
311 help=('File bugs on test failures. Must pass "True" or '
312 '"False" if used.'))
313 parser.add_argument("-l", "--bypass_labstatus", dest="bypass_labstatus",
314 action="store_true", help='Bypass lab status check.')
Alex Miller88762a82013-09-04 15:41:28 -0700315 # We allow either a number or a string for the priority. This way, if you
316 # know what you're doing, one can specify a custom priority level between
317 # other levels.
Allen Li93f4db52016-09-14 14:44:59 -0700318 parser.add_argument("-r", "--priority", dest="priority",
Allen Li603728a2016-12-08 13:58:11 -0800319 type=_get_priority_value,
Allen Li93f4db52016-09-14 14:44:59 -0700320 default=priorities.Priority.DEFAULT,
321 action="store",
322 help="Priority of suite. Either numerical value, or "
323 "one of (" + ", ".join(priorities.Priority.names)
324 + ").")
325 parser.add_argument(
326 '--retry', dest='retry', default=False, type=bool_str, action='store',
327 help='Enable test retry. Must pass "True" or "False" if used.')
328 parser.add_argument('--max_retries', dest='max_retries', default=None,
329 type=int, action='store', help='Maximum retries'
330 'allowed at suite level. No limit if not specified.')
331 parser.add_argument('--minimum_duts', dest='minimum_duts', type=int,
332 default=0, action='store',
333 help='Check that the pool has at least such many '
334 'healthy machines, otherwise suite will not run. '
335 'Default to 0.')
336 parser.add_argument('--suite_min_duts', dest='suite_min_duts', type=int,
337 default=0, action='store',
338 help='Preferred minimum number of machines. Scheduler '
339 'will prioritize on getting such many machines for '
340 'the suite when it is competing with another suite '
341 'that has a higher priority but already got minimum '
342 'machines it needs. Default to 0.')
343 parser.add_argument("--suite_args", dest="suite_args",
Allen Liecdba6c2017-07-11 12:10:26 -0700344 type=ast.literal_eval,
Allen Li93f4db52016-09-14 14:44:59 -0700345 default=None, action="store",
Allen Liecdba6c2017-07-11 12:10:26 -0700346 help="A dict of args passed to the suite control file.")
Allen Li93f4db52016-09-14 14:44:59 -0700347 parser.add_argument('--offload_failures_only',
Allen Li40599a32016-12-08 13:23:35 -0800348 dest='offload_failures_only', type=bool_str,
349 action='store', default=False,
Allen Li93f4db52016-09-14 14:44:59 -0700350 help='Only enable gs_offloading for failed tests. '
351 'Successful tests will be deleted. Must pass "True"'
352 ' or "False" if used.')
353 parser.add_argument('--use_suite_attr', dest='use_suite_attr',
354 action='store_true', default=False,
355 help='Advanced. Run the suite based on ATTRIBUTES of '
356 'control files, rather than SUITE.')
357 parser.add_argument('--json_dump', dest='json_dump', action='store_true',
358 default=False,
359 help='Dump the output of run_suite to stdout.')
360 parser.add_argument(
361 '--run_prod_code', dest='run_prod_code',
362 action='store_true', default=False,
363 help='Run the test code that lives in prod aka the test '
364 'code currently on the lab servers.')
365 parser.add_argument(
366 '--delay_minutes', type=int, default=0,
367 help=('Delay the creation of test jobs for a given '
368 'number of minutes. This argument can be used to '
369 'force provision jobs being delayed, which helps '
370 'to distribute loads across devservers.'))
371 parser.add_argument(
372 '--skip_duts_check', dest='skip_duts_check', action='store_true',
373 default=False, help='If True, skip minimum available DUTs check')
Shuqian Zhao843ae5c72017-02-22 11:25:01 -0800374 parser.add_argument(
Shuqian Zhao637d22c2017-03-06 15:52:32 -0800375 '--job_keyvals', dest='job_keyvals', type=ast.literal_eval,
Shuqian Zhao843ae5c72017-02-22 11:25:01 -0800376 action='store', default=None,
377 help='A dict of job keyvals to be inject to suite control file')
Shuqian Zhaoed0da862017-03-06 14:47:13 -0800378 parser.add_argument(
379 '--test_args', dest='test_args', type=ast.literal_eval,
380 action='store', default=None,
381 help=('A dict of args passed all the way to each individual test that '
382 'will be actually ran.'))
xixuand3cb33d2017-07-07 14:47:53 -0700383 parser.add_argument(
xixuan99eba0b2017-07-12 15:10:01 -0700384 '--require_logfile', action='store_true',
xixuand3cb33d2017-07-07 14:47:53 -0700385 help=('Stream logs of run_suite.py to a local file named '
386 'run_suite-<build name>.log.'))
Aviv Keshet97bebd42017-05-24 21:02:32 -0700387
388 # Used for monitoring purposes, to measure no-op swarming proxy latency.
389 parser.add_argument('--do_nothing', action='store_true',
390 help=argparse.SUPPRESS)
391
xixuanae791b12017-06-29 15:40:19 -0700392 # Used when lab/job status checking is needed. Currently its only user is
393 # suite scheduler v2.
394 parser.add_argument(
395 '--pre_check', action='store_true',
396 help=('Check lab and job status before kicking off a suite. Used by '
397 'suite scheduler v2.'))
398
Allen Li02b46c52017-09-11 11:48:12 -0700399 # TODO(crbug.com/763207): This is to support calling old moblab RPC
400 # with ToT code. This does not need to be supported after M62.
401 parser.add_argument('--oldrpc', action='store_true',
402 help='Use old AFE RPC.')
403
Allen Li93f4db52016-09-14 14:44:59 -0700404 return parser
Chris Masone24b80f12012-02-14 14:18:01 -0800405
406
Allen Li85ae5df2017-07-10 14:58:16 -0700407def verify_and_clean_options(options):
Allen Li93f4db52016-09-14 14:44:59 -0700408 """Verify the validity of options.
Fang Dengdd20e452014-04-07 15:39:47 -0700409
Fang Dengdd20e452014-04-07 15:39:47 -0700410 @param options: The parsed options to verify.
Fang Dengdd20e452014-04-07 15:39:47 -0700411
412 @returns: True if verification passes, False otherwise.
413
414 """
Fang Deng6865aab2015-02-20 14:49:47 -0800415 if options.mock_job_id and (
416 not options.build or not options.name or not options.board):
417 print ('When using -m, need to specify build, board and suite '
418 'name which you have used for creating the original job')
419 return False
420 else:
Fang Dengdd20e452014-04-07 15:39:47 -0700421 if not options.build:
422 print 'Need to specify which build to use'
423 return False
424 if not options.board:
425 print 'Need to specify board'
426 return False
427 if not options.name:
428 print 'Need to specify suite name'
429 return False
Allen Li04afc8f2017-11-27 15:36:34 -0800430 if options.num is not None:
431 warnings.warn('-u/--num option is deprecated; it does nothing.')
432 del options.num
Allen Li93f4db52016-09-14 14:44:59 -0700433 if not options.retry and options.max_retries is not None:
Fang Deng443f1952015-01-02 14:51:49 -0800434 print 'max_retries can only be used with --retry=True'
435 return False
Shuqian Zhaoab1bedc2015-06-02 11:12:28 -0700436 if options.use_suite_attr and options.suite_args is not None:
437 print ('The new suite control file cannot parse the suite_args: %s.'
438 'Please not specify any suite_args here.' % options.suite_args)
439 return False
Allen Li93f4db52016-09-14 14:44:59 -0700440 if options.no_wait and options.retry:
Fang Deng058860c2014-05-15 15:41:50 -0700441 print 'Test retry is not available when using --no_wait=True'
Dan Shi36cfd832014-10-10 13:38:51 -0700442 # Default to use the test code in CrOS build.
443 if not options.test_source_build and options.build:
444 options.test_source_build = options.build
Fang Dengdd20e452014-04-07 15:39:47 -0700445 return True
446
447
Shuqian Zhaoab1bedc2015-06-02 11:12:28 -0700448def change_options_for_suite_attr(options):
449 """Change options to be prepared to run the suite_attr_wrapper.
450
451 If specify 'use_suite_attr' from the cmd line, it indicates to run the
452 new style suite control file, suite_attr_wrapper. Then, change the
Allen Li6a612392016-08-18 12:09:32 -0700453 options.name to 'suite_attr_wrapper', change the options.suite_args to
Shuqian Zhaoab1bedc2015-06-02 11:12:28 -0700454 include the arguments needed by suite_attr_wrapper.
455
456 @param options: The verified options.
457
458 @returns: The changed options.
459
460 """
461 # Convert the suite_name to attribute boolean expression.
462 if type(options.name) is str:
463 attr_filter_val = 'suite:%s' % options.name
464 else:
465 attr_filter_val = ' or '.join(['suite:%s' % x for x in options.name])
466
467 # change the suite_args to be a dict of arguments for suite_attr_wrapper
468 # if suite_args is not None, store the values in 'other_args' of the dict
469 args_dict = {}
470 args_dict['attr_filter'] = attr_filter_val
Allen Liecdba6c2017-07-11 12:10:26 -0700471 options.suite_args = args_dict
Shuqian Zhaoab1bedc2015-06-02 11:12:28 -0700472 options.name = 'suite_attr_wrapper'
473
474 return options
475
476
Allen Li34613242016-09-02 11:52:34 -0700477class TestResult(object):
Aviv Keshet1480c4a2013-03-21 16:38:31 -0700478
Allen Li34613242016-09-02 11:52:34 -0700479 """Represents the result of a TestView."""
Aviv Keshet1480c4a2013-03-21 16:38:31 -0700480
Allen Li34613242016-09-02 11:52:34 -0700481 def __init__(self, test_view, retry_count=0):
482 """Initialize instance.
483
484 @param test_view: TestView instance.
485 @param retry_count: Retry count for test. Optional.
486 """
487 self.name = test_view.get_testname()
488 self.status = test_view['status']
489 self.reason = test_view['reason']
490 self.retry_count = retry_count
491
492 _PRETTY_STATUS_MAP = {
493 'GOOD': '[ PASSED ]',
494 'TEST_NA': '[ INFO ]',
495 }
496
497 @property
498 def _pretty_status(self):
499 """Pretty status string."""
500 return self._PRETTY_STATUS_MAP.get(self.status, '[ FAILED ]')
501
502 def log_using(self, log_function, name_column_width):
503 """Log the test result using the given log function.
504
505 @param log_function: Log function to use. Example: logging.info
506 @param name_column_width: Width of name column for formatting.
507 """
508 padded_name = self.name.ljust(name_column_width)
509 log_function('%s%s', padded_name, self._pretty_status)
510 if self.status != 'GOOD':
511 log_function('%s %s: %s', padded_name, self.status, self.reason)
512 if self.retry_count > 0:
513 log_function('%s retry_count: %s', padded_name, self.retry_count)
Chris Masone24b80f12012-02-14 14:18:01 -0800514
Fang Dengdd20e452014-04-07 15:39:47 -0700515
Shuqian Zhaof39bf2a2015-09-29 14:19:28 -0700516def get_original_suite_name(suite_name, suite_args):
517 """Get the original suite name when running suite_attr_wrapper.
518
519 @param suite_name: the name of the suite launched in afe. When it is
520 suite_attr_wrapper, the suite that actually running is
521 specified in the suite_args.
Allen Liecdba6c2017-07-11 12:10:26 -0700522 @param suite_args: dict of suite args from argument parsing.
Shuqian Zhaof39bf2a2015-09-29 14:19:28 -0700523
524 @returns: the original suite name.
525
526 """
527 if suite_name == 'suite_attr_wrapper':
Allen Liecdba6c2017-07-11 12:10:26 -0700528 attrs = suite_args.get('attr_filter', '')
Shuqian Zhaof39bf2a2015-09-29 14:19:28 -0700529 suite_list = ([x[6:] for x in re.split('[() ]', attrs)
530 if x and x.startswith('suite:')])
531 return suite_list[0] if suite_list else suite_name
532 return suite_name
533
534
Craig Harrison25eb0f32012-08-23 16:48:49 -0700535class LogLink(object):
J. Richard Barnetteb9c911d2013-08-23 11:24:21 -0700536 """Information needed to record a link in the logs.
Craig Harrison25eb0f32012-08-23 16:48:49 -0700537
J. Richard Barnetteb9c911d2013-08-23 11:24:21 -0700538 Depending on context and the information provided at
539 construction time, the link may point to either to log files for
540 a job, or to a bug filed for a failure in the job.
Craig Harrison25eb0f32012-08-23 16:48:49 -0700541
J. Richard Barnetteb9c911d2013-08-23 11:24:21 -0700542 @var anchor The link text.
543 @var url The link url.
544 @var bug_id Id of a bug to link to, or None.
545 """
546
Kevin Cheng2bdd3722016-03-24 21:30:52 -0700547 # A list of tests that don't get retried so skip the dashboard.
548 _SKIP_RETRY_DASHBOARD = ['provision']
549
Ningning Xiabd911bd2016-04-19 14:06:03 -0700550 _BUG_LINK_PREFIX = 'Auto-Bug'
551 _LOG_LINK_PREFIX = 'Test-Logs'
552
J. Richard Barnetteb9c911d2013-08-23 11:24:21 -0700553
Fang Dengaeab6172014-05-07 17:17:04 -0700554 def __init__(self, anchor, server, job_string, bug_info=None, reason=None,
Dan Shi9b620c22017-10-10 10:58:37 -0700555 retry_count=0, testname=None, sponge_url=None):
J. Richard Barnetteb9c911d2013-08-23 11:24:21 -0700556 """Initialize the LogLink by generating the log URL.
557
558 @param anchor The link text.
Alex Millerc7a59522013-10-30 15:18:57 -0700559 @param server The hostname of the server this suite ran on.
J. Richard Barnetteb9c911d2013-08-23 11:24:21 -0700560 @param job_string The job whose logs we'd like to link to.
561 @param bug_info Info about the bug, if one was filed.
Fang Deng53c6ff52014-02-24 17:51:24 -0800562 @param reason A string representing the reason of failure if any.
Fang Dengaeab6172014-05-07 17:17:04 -0700563 @param retry_count How many times the test has been retried.
Simran Basi7203d4e2015-02-03 15:50:18 -0800564 @param testname Optional Arg that supplies the testname.
Dan Shi9b620c22017-10-10 10:58:37 -0700565 @param sponge_url url to Sponge result.
Craig Harrison25eb0f32012-08-23 16:48:49 -0700566 """
567 self.anchor = anchor
Prathmesh Prabhucd246f52018-01-03 13:45:48 -0800568 self.url = _URL_PATTERN % (rpc_client_lib.add_protocol(server),
569 job_string)
Fang Deng53c6ff52014-02-24 17:51:24 -0800570 self.reason = reason
Fang Dengaeab6172014-05-07 17:17:04 -0700571 self.retry_count = retry_count
Simran Basi7203d4e2015-02-03 15:50:18 -0800572 self.testname = testname
Dan Shi9b620c22017-10-10 10:58:37 -0700573 self.sponge_url = sponge_url
J. Richard Barnetteb9c911d2013-08-23 11:24:21 -0700574 if bug_info:
575 self.bug_id, self.bug_count = bug_info
576 else:
577 self.bug_id = None
578 self.bug_count = None
Craig Harrison25eb0f32012-08-23 16:48:49 -0700579
580
Allen Lie082ced2016-09-14 15:19:20 -0700581 @property
582 def bug_url(self):
583 """URL of associated bug."""
584 if self.bug_id:
585 return reporting_utils.link_crbug(self.bug_id)
586 else:
587 return None
588
589
590 @property
591 def _bug_count_text(self):
592 """Return bug count as human friendly text."""
593 if self.bug_count is None:
594 bug_info = 'unknown number of reports'
595 elif self.bug_count == 1:
596 bug_info = 'new report'
597 else:
598 bug_info = '%s reports' % self.bug_count
599 return bug_info
600
601
Ningning Xiabd911bd2016-04-19 14:06:03 -0700602 def GenerateBuildbotLinks(self):
J. Richard Barnetteb9c911d2013-08-23 11:24:21 -0700603 """Generate a link formatted to meet buildbot expectations.
604
Ningning Xiabd911bd2016-04-19 14:06:03 -0700605 If there is a bug associated with this link, report a link to the bug
Allen Li4e7365e2017-07-10 15:40:24 -0700606 and a link to the job logs; otherwise report a link to the job logs.
Craig Harrison25eb0f32012-08-23 16:48:49 -0700607
Allen Li4e7365e2017-07-10 15:40:24 -0700608 @return A generator of links formatted for the buildbot log annotator.
Craig Harrison25eb0f32012-08-23 16:48:49 -0700609 """
Allen Li4e7365e2017-07-10 15:40:24 -0700610 if self.bug_url:
611 yield self._get_link_to_bug()
612 yield self._get_link_to_job_logs()
Ningning Xiabd911bd2016-04-19 14:06:03 -0700613
Allen Li4e7365e2017-07-10 15:40:24 -0700614
615 def _get_link_to_bug(self):
616 """Return buildbot link to bug.
617
618 @return A link formatted for the buildbot log annotator.
619 """
620 info_strings = self._get_info_strings()
621 info_strings.append(self._bug_count_text)
622 anchor_text = self._format_anchor_text(self._BUG_LINK_PREFIX,
623 info_strings)
624 return annotations.StepLink(anchor_text, self.bug_url)
625
626
627 def _get_link_to_job_logs(self):
628 """Return buildbot link to job logs.
629
630 @return A link formatted for the buildbot log annotator.
631 """
632 anchor_text = self._format_anchor_text(self._LOG_LINK_PREFIX,
633 self._get_info_strings())
634 return annotations.StepLink(anchor_text, self.url)
635
636
637 def _get_info_strings(self):
638 """Return a list of info strings for _format_anchor_text()."""
639 info_strings = []
Fang Dengaeab6172014-05-07 17:17:04 -0700640 if self.retry_count > 0:
641 info_strings.append('retry_count: %d' % self.retry_count)
Fang Deng53c6ff52014-02-24 17:51:24 -0800642 if self.reason:
Allen Lie082ced2016-09-14 15:19:20 -0700643 info_strings.append(self.reason)
Allen Li4e7365e2017-07-10 15:40:24 -0700644 return info_strings
Ningning Xiabd911bd2016-04-19 14:06:03 -0700645
646
Allen Lie082ced2016-09-14 15:19:20 -0700647 def _format_anchor_text(self, prefix, info_strings):
648 """Format anchor text given a prefix and info strings.
Ningning Xiabd911bd2016-04-19 14:06:03 -0700649
650 @param prefix The prefix of the anchor text.
Allen Lib1cb3842017-07-10 15:34:29 -0700651 @param info_strings Iterable of strings.
Ningning Xiabd911bd2016-04-19 14:06:03 -0700652 @return A anchor_text with the right prefix and info strings.
653 """
Allen Lib1cb3842017-07-10 15:34:29 -0700654 return '[{prefix}]: {anchor}: {info}'.format(
Allen Lie082ced2016-09-14 15:19:20 -0700655 prefix=prefix,
Allen Lib1cb3842017-07-10 15:34:29 -0700656 anchor=self.anchor.strip(),
657 info=', '.join(info_strings))
Craig Harrison25eb0f32012-08-23 16:48:49 -0700658
Allen Lie082ced2016-09-14 15:19:20 -0700659 @property
660 def text_link(self):
661 """Link to the job's logs, for consumption by a human.
Craig Harrison25eb0f32012-08-23 16:48:49 -0700662
Craig Harrisond8451572012-08-31 10:29:33 -0700663 @return A link formatted for human readability.
Craig Harrison25eb0f32012-08-23 16:48:49 -0700664 """
Aviv Keshet269848b2016-10-03 00:13:19 -0700665 return '%s %s' % (self.anchor, self.url)
Craig Harrison25eb0f32012-08-23 16:48:49 -0700666
Shuhei Takahashi18f56492017-11-14 16:23:46 +0900667 def GenerateRetryLink(self):
668 """Generate a link to the retry dashboard.
Simran Basi7203d4e2015-02-03 15:50:18 -0800669
670 @return A link formatted for the buildbot log annotator.
671 """
Allen Lie082ced2016-09-14 15:19:20 -0700672 if not self.testname or self.testname in self._SKIP_RETRY_DASHBOARD:
Simran Basi7203d4e2015-02-03 15:50:18 -0800673 return None
Xixuan Wu1729fca2018-03-21 16:32:58 -0700674
675 # TODO(xixuan): Return the right flake dashboard later.
676 return None
Simran Basi7203d4e2015-02-03 15:50:18 -0800677
Shuhei Takahashi18f56492017-11-14 16:23:46 +0900678 def GenerateHistoryLink(self):
679 """Generate a link to the test history dashboard.
David Rileya0cd1c22017-07-10 11:15:57 -0700680
681 @return A link formatted for the buildbot log annotator.
682 """
683 if not self.testname or self.testname in self._SKIP_RETRY_DASHBOARD:
684 return None
685 return annotations.StepLink(
686 text='[Test-History]: %s' % self.testname,
687 url=reporting_utils.link_test_history(self.testname))
688
Simran Basi7203d4e2015-02-03 15:50:18 -0800689
Chris Masoneb61b4052012-04-30 14:35:28 -0700690class Timings(object):
691 """Timings for important events during a suite.
692
693 All timestamps are datetime.datetime objects.
694
Fang Dengdd20e452014-04-07 15:39:47 -0700695 @var suite_job_id: the afe job id of the suite job for which
696 we are recording the timing for.
697 @var download_start_time: the time the devserver starts staging
698 the build artifacts. Recorded in create_suite_job.
699 @var payload_end_time: the time when the artifacts only necessary to start
700 installsing images onto DUT's are staged.
701 Recorded in create_suite_job.
702 @var artifact_end_time: the remaining artifacts are downloaded after we kick
703 off the reimaging job, at which point we record
704 artifact_end_time. Recorded in dynamic_suite.py.
Chris Masoneb61b4052012-04-30 14:35:28 -0700705 @var suite_start_time: the time the suite started.
Chris Masoneb61b4052012-04-30 14:35:28 -0700706 @var tests_start_time: the time the first test started running.
Fang Dengdd20e452014-04-07 15:39:47 -0700707 @var tests_end_time: the time the last test finished running.
Chris Masoneb61b4052012-04-30 14:35:28 -0700708 """
beeps6f02d192013-03-22 13:15:49 -0700709
Fang Dengdd20e452014-04-07 15:39:47 -0700710 def __init__(self, suite_job_id):
711 self.suite_job_id = suite_job_id
712 # Timings related to staging artifacts on devserver.
713 self.download_start_time = None
714 self.payload_end_time = None
715 self.artifact_end_time = None
beeps6f02d192013-03-22 13:15:49 -0700716
Fang Dengdd20e452014-04-07 15:39:47 -0700717 # The test_start_time, but taken off the view that corresponds to the
718 # suite instead of an individual test.
719 self.suite_start_time = None
beeps6f02d192013-03-22 13:15:49 -0700720
Fang Dengdd20e452014-04-07 15:39:47 -0700721 # Earliest and Latest tests in the set of TestViews passed to us.
722 self.tests_start_time = None
723 self.tests_end_time = None
724
Chris Masoneb61b4052012-04-30 14:35:28 -0700725
Chris Masoned9f13c52012-08-29 10:37:08 -0700726 def RecordTiming(self, view):
727 """Given a test report view, extract and record pertinent time info.
Chris Masoneb61b4052012-04-30 14:35:28 -0700728
729 get_detailed_test_views() returns a list of entries that provide
730 info about the various parts of a suite run. This method can take
731 any one of these entries and look up timestamp info we might want
732 and record it.
733
Chris Masonecfa7efc2012-09-06 16:00:07 -0700734 If timestamps are unavailable, datetime.datetime.min/max will be used.
735
Fang Dengaeab6172014-05-07 17:17:04 -0700736 @param view: A TestView object.
Chris Masoneb61b4052012-04-30 14:35:28 -0700737 """
Chris Masonecfa7efc2012-09-06 16:00:07 -0700738 start_candidate = datetime.min
739 end_candidate = datetime.max
740 if view['test_started_time']:
Dan Shidfea3682014-08-10 23:38:40 -0700741 start_candidate = time_utils.time_string_to_datetime(
742 view['test_started_time'])
Chris Masonecfa7efc2012-09-06 16:00:07 -0700743 if view['test_finished_time']:
Dan Shidfea3682014-08-10 23:38:40 -0700744 end_candidate = time_utils.time_string_to_datetime(
745 view['test_finished_time'])
Chris Masonecfa7efc2012-09-06 16:00:07 -0700746
Shuqian Zhaoc085abb2016-02-24 11:27:26 -0800747 if view.get_testname() == TestView.SUITE_JOB:
Chris Masoneb61b4052012-04-30 14:35:28 -0700748 self.suite_start_time = start_candidate
Chris Masoneb61b4052012-04-30 14:35:28 -0700749 else:
750 self._UpdateFirstTestStartTime(start_candidate)
751 self._UpdateLastTestEndTime(end_candidate)
Fang Dengdd20e452014-04-07 15:39:47 -0700752 if view['afe_job_id'] == self.suite_job_id and 'job_keyvals' in view:
Chris Masoned9f13c52012-08-29 10:37:08 -0700753 keyvals = view['job_keyvals']
Dan Shidfea3682014-08-10 23:38:40 -0700754 self.download_start_time = time_utils.time_string_to_datetime(
755 keyvals.get(constants.DOWNLOAD_STARTED_TIME),
756 handle_type_error=True)
beeps6f02d192013-03-22 13:15:49 -0700757
Dan Shidfea3682014-08-10 23:38:40 -0700758 self.payload_end_time = time_utils.time_string_to_datetime(
759 keyvals.get(constants.PAYLOAD_FINISHED_TIME),
760 handle_type_error=True)
beeps6f02d192013-03-22 13:15:49 -0700761
Dan Shidfea3682014-08-10 23:38:40 -0700762 self.artifact_end_time = time_utils.time_string_to_datetime(
763 keyvals.get(constants.ARTIFACT_FINISHED_TIME),
764 handle_type_error=True)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700765
Chris Masoneb61b4052012-04-30 14:35:28 -0700766
767 def _UpdateFirstTestStartTime(self, candidate):
768 """Update self.tests_start_time, iff candidate is an earlier time.
769
770 @param candidate: a datetime.datetime object.
771 """
772 if not self.tests_start_time or candidate < self.tests_start_time:
773 self.tests_start_time = candidate
774
775
776 def _UpdateLastTestEndTime(self, candidate):
777 """Update self.tests_end_time, iff candidate is a later time.
778
779 @param candidate: a datetime.datetime object.
780 """
781 if not self.tests_end_time or candidate > self.tests_end_time:
782 self.tests_end_time = candidate
783
784
785 def __str__(self):
786 return ('\n'
787 'Suite timings:\n'
Chris Masonea8066a92012-05-01 16:52:31 -0700788 'Downloads started at %s\n'
789 'Payload downloads ended at %s\n'
Chris Masoneb61b4052012-04-30 14:35:28 -0700790 'Suite started at %s\n'
Chris Masonea8066a92012-05-01 16:52:31 -0700791 'Artifact downloads ended (at latest) at %s\n'
Chris Masoneb61b4052012-04-30 14:35:28 -0700792 'Testing started at %s\n'
Chris Masonea8066a92012-05-01 16:52:31 -0700793 'Testing ended at %s\n' % (self.download_start_time,
794 self.payload_end_time,
795 self.suite_start_time,
Chris Masonea8066a92012-05-01 16:52:31 -0700796 self.artifact_end_time,
Chris Masoneb61b4052012-04-30 14:35:28 -0700797 self.tests_start_time,
798 self.tests_end_time))
799
800
Alex Millerc7a59522013-10-30 15:18:57 -0700801def instance_for_pool(pool_name):
802 """
803 Return the hostname of the server that should be used to service a suite
804 for the specified pool.
805
806 @param pool_name: The pool (without 'pool:' to schedule the suite against.
807 @return: The correct host that should be used to service this suite run.
808 """
809 return CONFIG.get_config_value(
810 'POOL_INSTANCE_SHARDING', pool_name,
811 default=_DEFAULT_AUTOTEST_INSTANCE)
812
813
Fang Dengaeab6172014-05-07 17:17:04 -0700814class TestView(object):
815 """Represents a test view and provides a set of helper functions."""
816
817
Shuqian Zhaoc085abb2016-02-24 11:27:26 -0800818 SUITE_JOB = 'Suite job'
Fang Dengaeab6172014-05-07 17:17:04 -0700819
820
Simran Basi17ca77c2015-10-14 19:05:00 -0700821 def __init__(self, view, afe_job, suite_name, build, user,
822 solo_test_run=False):
Fang Dengaeab6172014-05-07 17:17:04 -0700823 """Init a TestView object representing a tko test view.
824
825 @param view: A dictionary representing a tko test view.
Fang Dengf8503532014-06-12 18:21:55 -0700826 @param afe_job: An instance of frontend.afe.models.Job
827 representing the job that kicked off the test.
Fang Dengaeab6172014-05-07 17:17:04 -0700828 @param suite_name: The name of the suite
829 that the test belongs to.
830 @param build: The build for which the test is run.
Simran Basi01984f52015-10-12 15:36:45 -0700831 @param user: The user for which the test is run.
Simran Basi17ca77c2015-10-14 19:05:00 -0700832 @param solo_test_run: This is a solo test run not part of a suite.
Fang Dengaeab6172014-05-07 17:17:04 -0700833 """
834 self.view = view
Fang Dengf8503532014-06-12 18:21:55 -0700835 self.afe_job = afe_job
Fang Dengaeab6172014-05-07 17:17:04 -0700836 self.suite_name = suite_name
837 self.build = build
Simran Basi17ca77c2015-10-14 19:05:00 -0700838 self.is_suite_view = afe_job.parent_job is None and not solo_test_run
Fang Dengaeab6172014-05-07 17:17:04 -0700839 # This is the test name that will be shown in the output.
840 self.testname = None
Simran Basi01984f52015-10-12 15:36:45 -0700841 self.user = user
Fang Dengaeab6172014-05-07 17:17:04 -0700842
Fang Dengf8503532014-06-12 18:21:55 -0700843 # The case that a job was aborted before it got a chance to run
844 # usually indicates suite has timed out (unless aborted by user).
845 # In this case, the abort reason will be None.
846 # Update the reason with proper information.
847 if (self.is_relevant_suite_view() and
Shuqian Zhaoc085abb2016-02-24 11:27:26 -0800848 not self.get_testname() == self.SUITE_JOB and
Fang Dengf8503532014-06-12 18:21:55 -0700849 self.view['status'] == 'ABORT' and
850 not self.view['reason']):
851 self.view['reason'] = 'Timed out, did not run.'
852
Fang Dengaeab6172014-05-07 17:17:04 -0700853
854 def __getitem__(self, key):
855 """Overload __getitem__ so that we can still use []
856
857 @param key: A key of the tko test view.
858
859 @returns: The value of an attribute in the view.
860
861 """
862 return self.view[key]
863
864
Fang Dengaeab6172014-05-07 17:17:04 -0700865 def __iter__(self):
866 """Overload __iter__ so that it supports 'in' operator."""
867 return iter(self.view)
868
869
870 def get_testname(self):
871 """Get test name that should be shown in the output.
872
873 Formalize the test_name we got from the test view.
874
Allen Lie6236ec2017-07-05 12:52:36 -0700875 Remove 'build/suite' prefix if any.
Fang Dengaeab6172014-05-07 17:17:04 -0700876
877 If one runs a test in control file via the following code,
878 job.runtest('my_Test', tag='tag')
879 for most of the cases, view['test_name'] would look like 'my_Test.tag'.
880 If this is the case, this method will just return the original
881 test name, i.e. 'my_Test.tag'.
882
883 There are four special cases.
884 1) A test view is for the suite job's SERVER_JOB.
Shuqian Zhaoc085abb2016-02-24 11:27:26 -0800885 In this case, this method will return 'Suite job'.
Fang Dengaeab6172014-05-07 17:17:04 -0700886
Simran Basi17ca77c2015-10-14 19:05:00 -0700887 2) A test view is of a child job or a solo test run not part of a
888 suite, and for a SERVER_JOB or CLIENT_JOB.
Fang Dengaeab6172014-05-07 17:17:04 -0700889 In this case, we will take the job name, remove the build/suite
890 prefix from the job name, and append the rest to 'SERVER_JOB'
891 or 'CLIENT_JOB' as a prefix. So the names returned by this
892 method will look like:
Allen Lie6236ec2017-07-05 12:52:36 -0700893 'dummy_Pass_SERVER_JOB'
Fang Dengaeab6172014-05-07 17:17:04 -0700894 'dummy_Fail_SERVER_JOB'
895
Fang Dengf8503532014-06-12 18:21:55 -0700896 3) A test view is of a suite job and its status is ABORT.
Fang Dengaeab6172014-05-07 17:17:04 -0700897 In this case, the view['test_name'] is the child job's name.
Allen Lie6236ec2017-07-05 12:52:36 -0700898 For instance,
Allen Lie6236ec2017-07-05 12:52:36 -0700899 'lumpy-release/R35-5712.0.0/dummy/dummy_Pass'
Fang Dengaeab6172014-05-07 17:17:04 -0700900 'lumpy-release/R35-5712.0.0/dummy/dummy_Fail'
901 The above names will be converted to the following:
Allen Lie6236ec2017-07-05 12:52:36 -0700902 'dummy_Pass'
Fang Dengaeab6172014-05-07 17:17:04 -0700903 'dummy_Fail'
904
Fang Dengf8503532014-06-12 18:21:55 -0700905 4) A test view's status is of a suite job and its status is TEST_NA.
Fang Dengaeab6172014-05-07 17:17:04 -0700906 In this case, the view['test_name'] is the NAME field of the control
Allen Lie6236ec2017-07-05 12:52:36 -0700907 file. For instance,
Allen Lie6236ec2017-07-05 12:52:36 -0700908 'dummy_Pass'
Fang Dengaeab6172014-05-07 17:17:04 -0700909 'dummy_Fail'
910 This method will not modify these names.
911
912 @returns: Test name after normalization.
913
914 """
915 if self.testname is not None:
916 return self.testname
917
918 if (self.is_suite_view and
919 self.view['test_name'].startswith('SERVER_JOB')):
Shuqian Zhaoc085abb2016-02-24 11:27:26 -0800920 # Rename suite job's SERVER_JOB to 'Suite job'.
921 self.testname = self.SUITE_JOB
Fang Dengaeab6172014-05-07 17:17:04 -0700922 return self.testname
923
924 if (self.view['test_name'].startswith('SERVER_JOB') or
925 self.view['test_name'].startswith('CLIENT_JOB')):
926 # Append job name as a prefix for SERVER_JOB and CLIENT_JOB
927 testname= '%s_%s' % (self.view['job_name'], self.view['test_name'])
928 else:
929 testname = self.view['test_name']
Fang Dengaeab6172014-05-07 17:17:04 -0700930 # Remove the build and suite name from testname if any.
Allen Lie6236ec2017-07-05 12:52:36 -0700931 self.testname = tools.get_test_name(
Fang Dengaeab6172014-05-07 17:17:04 -0700932 self.build, self.suite_name, testname)
Fang Dengaeab6172014-05-07 17:17:04 -0700933 return self.testname
934
935
936 def is_relevant_suite_view(self):
937 """Checks whether this is a suite view we should care about.
938
939 @returns: True if it is relevant. False otherwise.
940 """
Shuqian Zhaoc085abb2016-02-24 11:27:26 -0800941 return (self.get_testname() == self.SUITE_JOB or
Fang Dengaeab6172014-05-07 17:17:04 -0700942 (self.is_suite_view and
943 not self.view['test_name'].startswith('CLIENT_JOB') and
944 not self.view['subdir']))
945
946
947 def is_test(self):
948 """Return whether the view is for an actual test.
949
950 @returns True if the view is for an actual test.
951 False if the view is for SERVER_JOB or CLIENT_JOB.
952
953 """
954 return not (self.view['test_name'].startswith('SERVER_JOB') or
955 self.view['test_name'].startswith('CLIENT_JOB'))
956
957
958 def is_retry(self):
959 """Check whether the view is for a retry.
960
961 @returns: True, if the view is for a retry; False otherwise.
962
963 """
964 return self.view['job_keyvals'].get('retry_original_job_id') is not None
965
966
Fang Dengf8503532014-06-12 18:21:55 -0700967 def hit_timeout(self):
968 """Check whether the corresponding job has hit its own timeout.
Fang Dengaeab6172014-05-07 17:17:04 -0700969
Fang Dengf8503532014-06-12 18:21:55 -0700970 Note this method should not be called for those test views
971 that belongs to a suite job and are determined as irrelevant
972 by is_relevant_suite_view. This is because they are associated
973 to the suite job, whose job start/finished time make no sense
974 to an irrelevant test view.
Fang Dengaeab6172014-05-07 17:17:04 -0700975
Fang Dengf8503532014-06-12 18:21:55 -0700976 @returns: True if the corresponding afe job has hit timeout.
977 False otherwise.
978 """
979 if (self.is_relevant_suite_view() and
Shuqian Zhaoc085abb2016-02-24 11:27:26 -0800980 self.get_testname() != self.SUITE_JOB):
981 # Any relevant suite test view except SUITE_JOB
Fang Dengf8503532014-06-12 18:21:55 -0700982 # did not hit its own timeout because it was not ever run.
983 return False
984 start = (datetime.strptime(
Dan Shidfea3682014-08-10 23:38:40 -0700985 self.view['job_started_time'], time_utils.TIME_FMT)
Fang Dengf8503532014-06-12 18:21:55 -0700986 if self.view['job_started_time'] else None)
987 end = (datetime.strptime(
Dan Shidfea3682014-08-10 23:38:40 -0700988 self.view['job_finished_time'], time_utils.TIME_FMT)
Fang Dengf8503532014-06-12 18:21:55 -0700989 if self.view['job_finished_time'] else None)
990 if not start or not end:
991 return False
992 else:
993 return ((end - start).total_seconds()/60.0
994 > self.afe_job.max_runtime_mins)
995
996
997 def is_aborted(self):
998 """Check if the view was aborted.
999
Shuqian Zhaoc085abb2016-02-24 11:27:26 -08001000 For suite job and child job test views, we check job keyval
Fang Dengf8503532014-06-12 18:21:55 -07001001 'aborted_by' and test status.
1002
1003 For relevant suite job test views, we only check test status
1004 because the suite job keyval won't make sense to individual
1005 test views.
1006
1007 @returns: True if the test was as aborted, False otherwise.
Fang Dengaeab6172014-05-07 17:17:04 -07001008
1009 """
Fang Dengf8503532014-06-12 18:21:55 -07001010
1011 if (self.is_relevant_suite_view() and
Shuqian Zhaoc085abb2016-02-24 11:27:26 -08001012 self.get_testname() != self.SUITE_JOB):
Fang Dengf8503532014-06-12 18:21:55 -07001013 return self.view['status'] == 'ABORT'
1014 else:
1015 return (bool(self.view['job_keyvals'].get('aborted_by')) and
1016 self.view['status'] in ['ABORT', 'RUNNING'])
Fang Dengaeab6172014-05-07 17:17:04 -07001017
1018
1019 def is_in_fail_status(self):
Fang Deng95af42f2014-09-12 14:16:11 -07001020 """Check if the given test's status corresponds to a failure.
Fang Dengaeab6172014-05-07 17:17:04 -07001021
1022 @returns: True if the test's status is FAIL or ERROR. False otherwise.
1023
1024 """
1025 # All the statuses tests can have when they fail.
1026 return self.view['status'] in ['FAIL', 'ERROR', 'ABORT']
1027
1028
Allen Licc205492017-07-10 17:26:04 -07001029 def is_provision(self):
1030 """Check whether this is a provision test."""
1031 return self.get_testname() == 'provision'
Fang Deng95af42f2014-09-12 14:16:11 -07001032
1033
Fang Dengaeab6172014-05-07 17:17:04 -07001034 def get_buildbot_link_reason(self):
1035 """Generate the buildbot link reason for the test.
1036
1037 @returns: A string representing the reason.
1038
1039 """
1040 return ('%s: %s' % (self.view['status'], self.view['reason'])
1041 if self.view['reason'] else self.view['status'])
1042
1043
1044 def get_job_id_owner_str(self):
1045 """Generate the job_id_owner string for a test.
1046
1047 @returns: A string which looks like 135036-username
1048
1049 """
Simran Basi01984f52015-10-12 15:36:45 -07001050 return '%s-%s' % (self.view['afe_job_id'], self.user)
Fang Dengaeab6172014-05-07 17:17:04 -07001051
1052
1053 def get_bug_info(self, suite_job_keyvals):
1054 """Get the bug info from suite_job_keyvals.
1055
1056 If a bug has been filed for the test, its bug info (bug id and counts)
1057 will be stored in the suite job's keyvals. This method attempts to
1058 retrieve bug info of the test from |suite_job_keyvals|. It will return
1059 None if no bug info is found. No need to check bug info if the view is
Shuqian Zhaoc085abb2016-02-24 11:27:26 -08001060 SUITE_JOB.
Fang Dengaeab6172014-05-07 17:17:04 -07001061
1062 @param suite_job_keyvals: The job keyval dictionary of the suite job.
1063 All the bug info about child jobs are stored in
1064 suite job's keyvals.
1065
1066 @returns: None if there is no bug info, or a pair with the
1067 id of the bug, and the count of the number of
1068 times the bug has been seen.
1069
1070 """
Shuqian Zhaoc085abb2016-02-24 11:27:26 -08001071 if self.get_testname() == self.SUITE_JOB:
Fang Dengaeab6172014-05-07 17:17:04 -07001072 return None
1073 if (self.view['test_name'].startswith('SERVER_JOB') or
1074 self.view['test_name'].startswith('CLIENT_JOB')):
1075 # Append job name as a prefix for SERVER_JOB and CLIENT_JOB
1076 testname= '%s_%s' % (self.view['job_name'], self.view['test_name'])
1077 else:
1078 testname = self.view['test_name']
1079
1080 return tools.get_test_failure_bug_info(
1081 suite_job_keyvals, self.view['afe_job_id'],
1082 testname)
1083
1084
1085 def should_display_buildbot_link(self):
1086 """Check whether a buildbot link should show for this view.
1087
Shuqian Zhaoc085abb2016-02-24 11:27:26 -08001088 For suite job view, show buildbot link if it fails.
Fang Dengf8503532014-06-12 18:21:55 -07001089 For normal test view,
1090 show buildbot link if it is a retry
1091 show buildbot link if it hits its own timeout.
1092 show buildbot link if it fails. This doesn't
1093 include the case where it was aborted but has
1094 not hit its own timeout (most likely it was aborted because
1095 suite has timed out).
Fang Dengaeab6172014-05-07 17:17:04 -07001096
1097 @returns: True if we should show the buildbot link.
1098 False otherwise.
1099 """
1100 is_bad_status = (self.view['status'] != 'GOOD' and
1101 self.view['status'] != 'TEST_NA')
Shuqian Zhaoc085abb2016-02-24 11:27:26 -08001102 if self.get_testname() == self.SUITE_JOB:
Fang Dengf8503532014-06-12 18:21:55 -07001103 return is_bad_status
1104 else:
1105 if self.is_retry():
1106 return True
1107 if is_bad_status:
1108 return not self.is_aborted() or self.hit_timeout()
Fang Dengaeab6172014-05-07 17:17:04 -07001109
1110
Shuqian Zhao2fecacd2015-08-05 22:56:30 -07001111 def get_control_file_attributes(self):
1112 """Get the attributes from the control file of the test.
1113
1114 @returns: A list of test attribute or None.
1115 """
1116 control_file = self.afe_job.control_file
1117 attributes = None
1118 if control_file:
1119 cd = control_data.parse_control_string(control_file)
1120 attributes = list(cd.attributes)
1121 return attributes
1122
1123
David Rileydcd1a642017-03-01 23:15:08 -08001124 def override_afe_job_id(self, afe_job_id):
1125 """Overrides the AFE job id for the test.
1126
1127 @param afe_job_id: The new AFE job id to use.
1128 """
1129 self.view['afe_job_id'] = afe_job_id
1130
1131
Allen Lidc2c69a2016-09-14 19:05:47 -07001132def log_buildbot_links(log_func, links):
1133 """Output buildbot links to log.
1134
1135 @param log_func: Logging function to use.
1136 @param links: Iterable of LogLink instances.
1137 """
1138 for link in links:
1139 for generated_link in link.GenerateBuildbotLinks():
1140 log_func(generated_link)
Shuhei Takahashi18f56492017-11-14 16:23:46 +09001141 retry_link = link.GenerateRetryLink()
1142 if retry_link:
1143 log_func(retry_link)
1144 history_link = link.GenerateHistoryLink()
1145 if history_link:
1146 log_func(history_link)
Allen Lidc2c69a2016-09-14 19:05:47 -07001147
1148
Allen Li0b675b62017-07-05 13:38:04 -07001149class _ReturnCodeComputer(object):
Allen Li422f24b2017-07-12 15:15:43 -07001150 """This is responsible for returning the _ReturnResult for a suite."""
Allen Li0b675b62017-07-05 13:38:04 -07001151
1152 def __call__(self, test_views):
1153 """Compute the exit code based on test results."""
Allen Licc205492017-07-10 17:26:04 -07001154 result = _RETURN_RESULTS['ok']
Allen Li0b675b62017-07-05 13:38:04 -07001155
1156 for v in test_views:
Allen Li0b675b62017-07-05 13:38:04 -07001157 if v.get_testname() == TestView.SUITE_JOB:
Allen Li422f24b2017-07-12 15:15:43 -07001158 result |= self._get_suite_result(v)
Allen Li0b675b62017-07-05 13:38:04 -07001159 else:
Allen Li422f24b2017-07-12 15:15:43 -07001160 result |= self._get_test_result(v)
1161 return result
1162
1163 def _get_suite_result(self, test_view):
1164 """Return the _ReturnResult for the given suite job."""
1165 # The order of checking each case is important.
1166 if test_view.is_aborted() and test_view.hit_timeout():
1167 return _RETURN_RESULTS['suite_timeout']
1168 elif test_view.is_in_fail_status():
1169 return _RETURN_RESULTS['suite_failed']
1170 elif test_view['status'] == 'WARN':
1171 return _RETURN_RESULTS['suite_warning']
1172 else:
1173 return _RETURN_RESULTS['ok']
1174
1175 def _get_test_result(self, test_view):
1176 """Return the _ReturnResult for the given test job."""
1177 # The order of checking each case is important.
1178 if test_view.is_aborted() and test_view.is_relevant_suite_view():
1179 # The test was aborted before started
1180 # This gurantees that the suite has timed out.
Prathmesh Prabhu316180c2017-12-19 16:06:44 -08001181 return _RETURN_RESULTS['test_aborted_prestart']
Allen Li422f24b2017-07-12 15:15:43 -07001182 elif test_view.is_aborted() and not test_view.hit_timeout():
1183 # The test was aborted, but
1184 # not due to a timeout. This is most likely
1185 # because the suite has timed out, but may
1186 # also because it was aborted by the user.
1187 # Since suite timing out is determined by checking
1188 # the suite job view, we simply ignore this view here.
Prathmesh Prabhu316180c2017-12-19 16:06:44 -08001189 return _RETURN_RESULTS['test_aborted_mystery']
Allen Li422f24b2017-07-12 15:15:43 -07001190 elif test_view.is_in_fail_status(): # The test job failed
1191 if test_view.is_provision():
1192 return _RETURN_RESULTS['provision_failed']
1193 else:
1194 return _RETURN_RESULTS['test_failure']
1195 elif test_view['status'] == 'WARN':
1196 return _RETURN_RESULTS['test_warning']
1197 elif test_view.is_retry():
1198 # The test is a passing retry.
1199 return _RETURN_RESULTS['test_retry']
1200 else:
1201 return _RETURN_RESULTS['ok']
1202
1203
1204class _ProvisionReturnCodeComputer(_ReturnCodeComputer):
1205 """This is used for returning the _ReturnResult for provision suites."""
1206
1207 def __init__(self, num_required):
1208 """Initialize instance.
1209
1210 num_required is the number of passing provision jobs needed.
1211 """
1212 super(_ProvisionReturnCodeComputer, self).__init__()
1213 self._num_required = num_required
1214 self._num_successful = 0
1215
1216 def __call__(self, test_views):
1217 result = super(_ProvisionReturnCodeComputer, self).__call__(test_views)
1218 if self._num_successful >= self._num_required:
1219 logging.info('Return result upgraded from %r'
1220 ' due to enough ok provisions',
1221 result)
1222 return _RETURN_RESULTS['ok']
1223 else:
1224 return result
1225
1226 def _get_test_result(self, test_view):
1227 result = (super(_ProvisionReturnCodeComputer, self)
1228 ._get_test_result(test_view))
1229 if result in {_RETURN_RESULTS[s] for s in ('ok', 'test_retry')}:
1230 self._num_successful += 1
Allen Licc205492017-07-10 17:26:04 -07001231 return result
Allen Li0b675b62017-07-05 13:38:04 -07001232
1233
Fang Dengdd20e452014-04-07 15:39:47 -07001234class ResultCollector(object):
Simran Basi17ca77c2015-10-14 19:05:00 -07001235 """Collect test results of a suite or a single test run.
Fang Dengdd20e452014-04-07 15:39:47 -07001236
1237 Once a suite job has finished, use this class to collect test results.
1238 `run` is the core method that is to be called first. Then the caller
1239 could retrieve information like return code, return message, is_aborted,
1240 and timings by accessing the collector's public attributes. And output
1241 the test results and links by calling the 'output_*' methods.
1242
1243 Here is a overview of what `run` method does.
1244
1245 1) Collect the suite job's results from tko_test_view_2.
1246 For the suite job, we only pull test views without a 'subdir'.
1247 A NULL subdir indicates that the test was _not_ executed. This could be
1248 that no child job was scheduled for this test or the child job got
1249 aborted before starts running.
1250 (Note 'SERVER_JOB'/'CLIENT_JOB' are handled specially)
1251
1252 2) Collect the child jobs' results from tko_test_view_2.
1253 For child jobs, we pull all the test views associated with them.
Allen Lidc2c69a2016-09-14 19:05:47 -07001254 (Note 'SERVER_JOB'/'CLIENT_JOB' are handled specially)
Fang Dengdd20e452014-04-07 15:39:47 -07001255
Fang Dengaeab6172014-05-07 17:17:04 -07001256 3) Generate web and buildbot links.
Fang Dengdd20e452014-04-07 15:39:47 -07001257 4) Compute timings of the suite run.
1258 5) Compute the return code based on test results.
1259
1260 @var _instance_server: The hostname of the server that is used
1261 to service the suite.
1262 @var _afe: The afe rpc client.
1263 @var _tko: The tko rpc client.
1264 @var _build: The build for which the suite is run,
1265 e.g. 'lumpy-release/R35-5712.0.0'
MK Ryu977a9752014-10-21 11:58:09 -07001266 @var _board: The target board for which the suite is run,
1267 e.g., 'lumpy', 'link'.
Fang Dengdd20e452014-04-07 15:39:47 -07001268 @var _suite_name: The suite name, e.g. 'bvt', 'dummy'.
1269 @var _suite_job_id: The job id of the suite for which we are going to
1270 collect results.
Shuqian Zhaof39bf2a2015-09-29 14:19:28 -07001271 @var _original_suite_name: The suite name we record timing would be
1272 different from _suite_name when running
1273 suite_attr_wrapper.
Allen Li0b675b62017-07-05 13:38:04 -07001274 @var _return_code_function: Called to return what the overall result of
1275 the suite is.
Fang Dengaeab6172014-05-07 17:17:04 -07001276 @var _suite_views: A list of TestView objects, representing relevant
1277 test views of the suite job.
1278 @var _child_views: A list of TestView objects, representing test views
1279 of the child jobs.
1280 @var _test_views: A list of TestView objects, representing all test views
1281 from _suite_views and _child_views.
Fang Dengdd20e452014-04-07 15:39:47 -07001282 @var _web_links: A list of web links pointing to the results of jobs.
Allen Li28be0642017-07-10 15:16:26 -07001283 @var buildbot_links: A list of buildbot links for non-passing tests.
Simran Basi17ca77c2015-10-14 19:05:00 -07001284 @var _solo_test_run: True if this is a single test run.
Allen Licc205492017-07-10 17:26:04 -07001285 @var return_result: The _ReturnResult of the suite run.
Fang Dengdd20e452014-04-07 15:39:47 -07001286 @var is_aborted: Whether the suite was aborted or not.
1287 True, False or None (aborting status is unknown yet)
1288 @var timings: A Timing object that records the suite's timings.
1289
1290 """
1291
1292
MK Ryu977a9752014-10-21 11:58:09 -07001293 def __init__(self, instance_server, afe, tko, build, board,
Allen Li0b675b62017-07-05 13:38:04 -07001294 suite_name, suite_job_id,
1295 return_code_function,
1296 original_suite_name=None,
Simran Basi17ca77c2015-10-14 19:05:00 -07001297 user=None, solo_test_run=False):
Fang Dengdd20e452014-04-07 15:39:47 -07001298 self._instance_server = instance_server
1299 self._afe = afe
1300 self._tko = tko
1301 self._build = build
MK Ryu977a9752014-10-21 11:58:09 -07001302 self._board = board
Fang Dengdd20e452014-04-07 15:39:47 -07001303 self._suite_name = suite_name
1304 self._suite_job_id = suite_job_id
Shuqian Zhaof39bf2a2015-09-29 14:19:28 -07001305 self._original_suite_name = original_suite_name or suite_name
Allen Li0b675b62017-07-05 13:38:04 -07001306 self._return_code_function = return_code_function
Fang Deng0454e632014-04-07 15:39:47 -07001307 self._suite_views = []
1308 self._child_views = []
Fang Dengdd20e452014-04-07 15:39:47 -07001309 self._test_views = []
Fang Dengaeab6172014-05-07 17:17:04 -07001310 self._retry_counts = {}
David Rileydcd1a642017-03-01 23:15:08 -08001311 self._missing_results = {}
Fang Dengdd20e452014-04-07 15:39:47 -07001312 self._web_links = []
Allen Li28be0642017-07-10 15:16:26 -07001313 self.buildbot_links = []
MK Ryu977a9752014-10-21 11:58:09 -07001314 self._num_child_jobs = 0
Allen Licc205492017-07-10 17:26:04 -07001315 self.return_result = None
Fang Dengdd20e452014-04-07 15:39:47 -07001316 self.is_aborted = None
1317 self.timings = None
Simran Basi01984f52015-10-12 15:36:45 -07001318 self._user = user or getpass.getuser()
Simran Basi17ca77c2015-10-14 19:05:00 -07001319 self._solo_test_run = solo_test_run
Fang Dengdd20e452014-04-07 15:39:47 -07001320
1321
Fang Dengdd20e452014-04-07 15:39:47 -07001322 def _fetch_relevant_test_views_of_suite(self):
1323 """Fetch relevant test views of the suite job.
1324
1325 For the suite job, there will be a test view for SERVER_JOB, and views
Allen Lidc2c69a2016-09-14 19:05:47 -07001326 for results of its child jobs. For example, assume we've created
Fang Dengdd20e452014-04-07 15:39:47 -07001327 a suite job (afe_job_id: 40) that runs dummy_Pass, dummy_Fail,
1328 dummy_Pass.bluetooth. Assume dummy_Pass was aborted before running while
1329 dummy_Path.bluetooth got TEST_NA as no duts have bluetooth.
1330 So the suite job's test views would look like
1331 _____________________________________________________________________
1332 test_idx| job_idx|test_name |subdir |afe_job_id|status
1333 10 | 1000 |SERVER_JOB |---- |40 |GOOD
1334 11 | 1000 |dummy_Pass |NULL |40 |ABORT
1335 12 | 1000 |dummy_Fail.Fail |41-onwer/...|40 |FAIL
1336 13 | 1000 |dummy_Fail.Error |42-owner/...|40 |ERROR
1337 14 | 1000 |dummy_Pass.bluetooth|NULL |40 |TEST_NA
1338
1339 For a suite job, we only care about
1340 a) The test view for the suite job's SERVER_JOB
1341 b) The test views for real tests without a subdir. A NULL subdir
1342 indicates that a test didn't get executed.
1343 So, for the above example, we only keep test views whose test_idxs
1344 are 10, 11, 14.
1345
Fang Dengaeab6172014-05-07 17:17:04 -07001346 @returns: A list of TestView objects, representing relevant
1347 test views of the suite job.
Fang Dengdd20e452014-04-07 15:39:47 -07001348
1349 """
Fang Dengf8503532014-06-12 18:21:55 -07001350 suite_job = self._afe.get_jobs(id=self._suite_job_id)[0]
Fang Deng0454e632014-04-07 15:39:47 -07001351 views = self._tko.run(call='get_detailed_test_views',
1352 afe_job_id=self._suite_job_id)
Fang Dengdd20e452014-04-07 15:39:47 -07001353 relevant_views = []
1354 for v in views:
Simran Basi17ca77c2015-10-14 19:05:00 -07001355 v = TestView(v, suite_job, self._suite_name, self._build, self._user,
1356 solo_test_run=self._solo_test_run)
Fang Dengaeab6172014-05-07 17:17:04 -07001357 if v.is_relevant_suite_view():
David Rileydcd1a642017-03-01 23:15:08 -08001358 # If the test doesn't have results in TKO and is being
1359 # displayed in the suite view instead of the child view,
1360 # then afe_job_id is incorrect and from the suite.
1361 # Override it based on the AFE job id which was missing
1362 # results.
1363 # TODO: This is likely inaccurate if a test has multiple
1364 # tries which all fail TKO parse stage.
1365 if v['test_name'] in self._missing_results:
1366 v.override_afe_job_id(
1367 self._missing_results[v['test_name']][0])
Fang Dengdd20e452014-04-07 15:39:47 -07001368 relevant_views.append(v)
Fang Dengdd20e452014-04-07 15:39:47 -07001369 return relevant_views
1370
1371
Fang Dengaeab6172014-05-07 17:17:04 -07001372 def _compute_retry_count(self, view):
1373 """Return how many times the test has been retried.
1374
1375 @param view: A TestView instance.
1376 @returns: An int value indicating the retry count.
1377
1378 """
1379 old_job = view['job_keyvals'].get('retry_original_job_id')
1380 count = 0
1381 while old_job:
1382 count += 1
1383 views = self._tko.run(
1384 call='get_detailed_test_views', afe_job_id=old_job)
1385 old_job = (views[0]['job_keyvals'].get('retry_original_job_id')
1386 if views else None)
1387 return count
1388
1389
Simran Basi17ca77c2015-10-14 19:05:00 -07001390 def _fetch_test_views_of_child_jobs(self, jobs=None):
Fang Dengdd20e452014-04-07 15:39:47 -07001391 """Fetch test views of child jobs.
1392
David Rileydcd1a642017-03-01 23:15:08 -08001393 @returns: A tuple (child_views, retry_counts, missing_results)
Fang Dengaeab6172014-05-07 17:17:04 -07001394 child_views is list of TestView objects, representing
David Rileydcd1a642017-03-01 23:15:08 -08001395 all valid views.
1396 retry_counts is a dictionary that maps test_idx to retry
1397 counts. It only stores retry counts that are greater than 0.
1398 missing_results is a dictionary that maps test names to
1399 lists of job ids.
Fang Deng0454e632014-04-07 15:39:47 -07001400
Fang Dengdd20e452014-04-07 15:39:47 -07001401 """
Fang Dengdd20e452014-04-07 15:39:47 -07001402 child_views = []
Fang Dengaeab6172014-05-07 17:17:04 -07001403 retry_counts = {}
David Rileydcd1a642017-03-01 23:15:08 -08001404 missing_results = {}
Simran Basi17ca77c2015-10-14 19:05:00 -07001405 child_jobs = jobs or self._afe.get_jobs(parent_job_id=self._suite_job_id)
MK Ryu977a9752014-10-21 11:58:09 -07001406 if child_jobs:
1407 self._num_child_jobs = len(child_jobs)
Fang Dengf8503532014-06-12 18:21:55 -07001408 for job in child_jobs:
Simran Basi01984f52015-10-12 15:36:45 -07001409 views = [TestView(v, job, self._suite_name, self._build, self._user)
Fang Dengaeab6172014-05-07 17:17:04 -07001410 for v in self._tko.run(
Fang Dengf8503532014-06-12 18:21:55 -07001411 call='get_detailed_test_views', afe_job_id=job.id,
Fang Dengaeab6172014-05-07 17:17:04 -07001412 invalid=0)]
David Rileydcd1a642017-03-01 23:15:08 -08001413 if len(views) == 0:
1414 missing_results.setdefault(job.name, []).append(job.id)
Fang Dengdd20e452014-04-07 15:39:47 -07001415 contains_test_failure = any(
Fang Dengaeab6172014-05-07 17:17:04 -07001416 v.is_test() and v['status'] != 'GOOD' for v in views)
Fang Dengdd20e452014-04-07 15:39:47 -07001417 for v in views:
Fang Dengaeab6172014-05-07 17:17:04 -07001418 if (v.is_test() or
1419 v['status'] != 'GOOD' and not contains_test_failure):
1420 # For normal test view, just keep it.
1421 # For SERVER_JOB or CLIENT_JOB, only keep it
1422 # if it fails and no other test failure.
Fang Dengdd20e452014-04-07 15:39:47 -07001423 child_views.append(v)
Fang Dengaeab6172014-05-07 17:17:04 -07001424 retry_count = self._compute_retry_count(v)
1425 if retry_count > 0:
1426 retry_counts[v['test_idx']] = retry_count
David Rileydcd1a642017-03-01 23:15:08 -08001427 return child_views, retry_counts, missing_results
Fang Dengdd20e452014-04-07 15:39:47 -07001428
1429
1430 def _generate_web_and_buildbot_links(self):
1431 """Generate web links and buildbot links."""
1432 # TODO(fdeng): If a job was aborted before it reaches Running
1433 # state, we read the test view from the suite job
1434 # and thus this method generates a link pointing to the
1435 # suite job's page for the aborted job. Need a fix.
1436 self._web_links = []
Allen Li28be0642017-07-10 15:16:26 -07001437 self.buildbot_links = []
Aviv Kesheta336b9f2018-01-26 11:23:11 -08001438
Fang Dengdd20e452014-04-07 15:39:47 -07001439 # Bug info are stored in the suite job's keyvals.
Simran Basi17ca77c2015-10-14 19:05:00 -07001440 if self._solo_test_run:
1441 suite_job_keyvals = {}
Aviv Kesheta336b9f2018-01-26 11:23:11 -08001442 elif not self._suite_views:
1443 suite_job_keyvals = {}
Simran Basi17ca77c2015-10-14 19:05:00 -07001444 else:
1445 suite_job_keyvals = self._suite_views[0]['job_keyvals']
Aviv Kesheta336b9f2018-01-26 11:23:11 -08001446
Fang Dengdd20e452014-04-07 15:39:47 -07001447 for v in self._test_views:
Fang Dengaeab6172014-05-07 17:17:04 -07001448 retry_count = self._retry_counts.get(v['test_idx'], 0)
1449 bug_info = v.get_bug_info(suite_job_keyvals)
1450 job_id_owner = v.get_job_id_owner_str()
Fang Dengdd20e452014-04-07 15:39:47 -07001451 link = LogLink(
Allen Li34613242016-09-02 11:52:34 -07001452 anchor=v.get_testname(),
Fang Dengdd20e452014-04-07 15:39:47 -07001453 server=self._instance_server,
1454 job_string=job_id_owner,
Simran Basi7203d4e2015-02-03 15:50:18 -08001455 bug_info=bug_info, retry_count=retry_count,
Dan Shi9b620c22017-10-10 10:58:37 -07001456 testname=v.get_testname(),
1457 sponge_url=suite_job_keyvals.get('sponge_url'))
Fang Dengdd20e452014-04-07 15:39:47 -07001458 self._web_links.append(link)
1459
Fang Dengaeab6172014-05-07 17:17:04 -07001460 if v.should_display_buildbot_link():
1461 link.reason = v.get_buildbot_link_reason()
Allen Li28be0642017-07-10 15:16:26 -07001462 self.buildbot_links.append(link)
Fang Dengdd20e452014-04-07 15:39:47 -07001463
1464
1465 def _record_timings(self):
1466 """Record suite timings."""
1467 self.timings = Timings(self._suite_job_id)
1468 for v in self._test_views:
1469 self.timings.RecordTiming(v)
1470
1471
1472 def _compute_return_code(self):
1473 """Compute the exit code based on test results."""
Allen Licc205492017-07-10 17:26:04 -07001474 self.return_result = self._return_code_function(self._test_views)
Fang Dengdd20e452014-04-07 15:39:47 -07001475
1476
Allen Li34613242016-09-02 11:52:34 -07001477 def _make_test_results(self):
1478 """Make TestResults for collected tests.
1479
1480 @returns: List of TestResult instances.
1481 """
1482 test_results = []
1483 for test_view in self._test_views:
1484 test_result = TestResult(
1485 test_view=test_view,
1486 retry_count=self._retry_counts.get(test_view['test_idx'], 0))
1487 test_results.append(test_result)
1488 return test_results
1489
1490
Fang Dengdd20e452014-04-07 15:39:47 -07001491 def output_results(self):
1492 """Output test results, timings and web links."""
1493 # Output test results
Allen Li34613242016-09-02 11:52:34 -07001494 test_results = self._make_test_results()
Brian Norrisc7575d32017-09-25 17:08:19 -07001495 if len(test_results) == 0:
1496 max_name_length = 0
1497 else:
1498 max_name_length = max(len(t.name) for t in test_results)
Allen Li34613242016-09-02 11:52:34 -07001499 for test_result in test_results:
1500 test_result.log_using(logging.info, max_name_length + 3)
Fang Dengdd20e452014-04-07 15:39:47 -07001501 # Output suite timings
1502 logging.info(self.timings)
1503 # Output links to test logs
1504 logging.info('\nLinks to test logs:')
1505 for link in self._web_links:
Allen Lie082ced2016-09-14 15:19:20 -07001506 logging.info(link.text_link)
Fang Deng5a43be62014-05-07 17:17:04 -07001507 logging.info('\n')
Fang Dengdd20e452014-04-07 15:39:47 -07001508
1509
Shuqian Zhao2fecacd2015-08-05 22:56:30 -07001510 def get_results_dict(self):
1511 """Write test results, timings and web links into a dict.
1512
1513 @returns: A dict of results in the format like:
1514 {
1515 'tests': {
1516 'test_1': {'status': 'PASSED', 'attributes': [1,2], ...}
1517 'test_2': {'status': 'FAILED', 'attributes': [1],...}
1518 }
1519 'suite_timings': {
1520 'download_start': '1998-07-17 00:00:00',
1521 'payload_download_end': '1998-07-17 00:00:05',
1522 ...
1523 }
1524 }
1525 """
1526 output_dict = {}
1527 tests_dict = output_dict.setdefault('tests', {})
1528 for v in self._test_views:
Shuqian Zhaofae149c2017-01-30 16:46:53 -08001529 test_name = v.get_testname()
1530 test_info = tests_dict.setdefault(test_name, {})
1531 test_info.update({
1532 'status': v['status'],
1533 'attributes': v.get_control_file_attributes() or list(),
1534 'reason': v['reason'],
1535 'retry_count': self._retry_counts.get(v['test_idx'], 0),
1536 })
1537 # For aborted test, the control file will not be parsed and thus
1538 # fail to get the attributes info. Therefore, the subsystems the
1539 # abort test testing will be missing. For this case, we will assume
1540 # the aborted test will test all subsystems, set subsystem:default.
1541 if (test_info['status'] == 'ABORT' and
1542 not any('subsystem:' in a for a in test_info['attributes'])):
1543 test_info['attributes'].append('subsystem:default')
Shuqian Zhao2fecacd2015-08-05 22:56:30 -07001544
1545 # Write the links to test logs into the |tests_dict| of |output_dict|.
1546 # For test whose status is not 'GOOD', the link is also buildbot_link.
1547 for link in self._web_links:
Shuqian Zhaofae149c2017-01-30 16:46:53 -08001548 test_name = link.anchor.strip()
1549 test_info = tests_dict.get(test_name)
1550 if test_info:
1551 test_info['link_to_logs'] = link.url
Dan Shi9b620c22017-10-10 10:58:37 -07001552 test_info['sponge_url'] = link.sponge_url
Shuhei Takahashi18f56492017-11-14 16:23:46 +09001553 # Write the retry dashboard link into the dict.
Allen Li28be0642017-07-10 15:16:26 -07001554 if link in self.buildbot_links and link.testname:
Shuhei Takahashi18f56492017-11-14 16:23:46 +09001555 test_info['retry_dashboard_link'] \
Shuqian Zhaofae149c2017-01-30 16:46:53 -08001556 = reporting_utils.link_retry_url(link.testname)
Shuhei Takahashi18f56492017-11-14 16:23:46 +09001557 # Always write the wmatrix link for compatibility.
1558 test_info['wmatrix_link'] \
1559 = reporting_utils.link_wmatrix_retry_url(link.testname)
Shuqian Zhaofae149c2017-01-30 16:46:53 -08001560 # Write the bug url into the dict.
1561 if link.bug_id:
1562 test_info['bug_url'] = link.bug_url
Shuqian Zhao2fecacd2015-08-05 22:56:30 -07001563
1564 # Write the suite timings into |output_dict|
Allen Li2c5d44b2016-08-15 17:58:58 -07001565 timings = self.timings
1566 if timings is not None:
1567 time_dict = output_dict.setdefault('suite_timings', {})
1568 time_dict.update({
1569 'download_start' : str(timings.download_start_time),
1570 'payload_download_end' : str(timings.payload_end_time),
1571 'suite_start' : str(timings.suite_start_time),
1572 'artifact_download_end' : str(timings.artifact_end_time),
1573 'tests_start' : str(timings.tests_start_time),
1574 'tests_end' : str(timings.tests_end_time),
1575 })
Shuqian Zhao2fecacd2015-08-05 22:56:30 -07001576
1577 output_dict['suite_job_id'] = self._suite_job_id
1578
1579 return output_dict
1580
1581
Fang Dengdd20e452014-04-07 15:39:47 -07001582 def run(self):
1583 """Collect test results.
1584
1585 This method goes through the following steps:
1586 Fetch relevent test views of the suite job.
1587 Fetch test views of child jobs
1588 Check whether the suite was aborted.
Fang Dengaeab6172014-05-07 17:17:04 -07001589 Generate links.
Fang Dengdd20e452014-04-07 15:39:47 -07001590 Calculate suite timings.
1591 Compute return code based on the test result.
1592
1593 """
Simran Basi17ca77c2015-10-14 19:05:00 -07001594 if self._solo_test_run:
Allen Li29f5e242017-07-10 15:00:57 -07001595 self._test_views, self._retry_counts, self._missing_results = (
Simran Basi17ca77c2015-10-14 19:05:00 -07001596 self._fetch_test_views_of_child_jobs(
1597 jobs=self._afe.get_jobs(id=self._suite_job_id)))
1598 else:
David Rileydcd1a642017-03-01 23:15:08 -08001599 self._child_views, self._retry_counts, self._missing_results = (
Simran Basi17ca77c2015-10-14 19:05:00 -07001600 self._fetch_test_views_of_child_jobs())
David Rileydcd1a642017-03-01 23:15:08 -08001601 self._suite_views = self._fetch_relevant_test_views_of_suite()
Simran Basi17ca77c2015-10-14 19:05:00 -07001602 self._test_views = self._suite_views + self._child_views
Fang Dengdd20e452014-04-07 15:39:47 -07001603 # For hostless job in Starting status, there is no test view associated.
1604 # This can happen when a suite job in Starting status is aborted. When
1605 # the scheduler hits some limit, e.g., max_hostless_jobs_per_drone,
1606 # max_jobs_started_per_cycle, a suite job can stays in Starting status.
1607 if not self._test_views:
Allen Licc205492017-07-10 17:26:04 -07001608 self.return_result = _RETURN_RESULTS['test_views_missing']
Fang Dengdd20e452014-04-07 15:39:47 -07001609 return
1610 self.is_aborted = any([view['job_keyvals'].get('aborted_by')
1611 for view in self._suite_views])
Fang Dengdd20e452014-04-07 15:39:47 -07001612 self._generate_web_and_buildbot_links()
1613 self._record_timings()
1614 self._compute_return_code()
1615
1616
MK Ryu977a9752014-10-21 11:58:09 -07001617 def gather_timing_stats(self):
1618 """Collect timing related statistics."""
MK Ryu977a9752014-10-21 11:58:09 -07001619 # Record suite runtime in metadata db.
Prathmesh Prabhua3713a02015-03-11 13:50:55 -07001620 # Some failure modes can leave times unassigned, report sentinel value
1621 # in that case.
1622 runtime_in_secs = -1
1623 if (self.timings.tests_end_time is not None and
1624 self.timings.suite_start_time is not None):
Dan Shi0723bf52015-06-24 10:52:38 -07001625 runtime_in_secs = (self.timings.tests_end_time -
1626 self.timings.suite_start_time).total_seconds()
Prathmesh Prabhua3713a02015-03-11 13:50:55 -07001627
MK Ryu977a9752014-10-21 11:58:09 -07001628 job_overhead.record_suite_runtime(self._suite_job_id, self._suite_name,
1629 self._board, self._build, self._num_child_jobs, runtime_in_secs)
1630
1631
Allen Li0b675b62017-07-05 13:38:04 -07001632
Allen Li0716efa2016-12-08 13:51:31 -08001633def _make_builds_from_options(options):
1634 """Create a dict of builds for creating a suite job.
Prashanth B6285f6a2014-05-08 18:01:27 -07001635
Allen Li0716efa2016-12-08 13:51:31 -08001636 The returned dict maps version label prefixes to build names. Together,
1637 each key-value pair describes a complete label.
Prashanth B6285f6a2014-05-08 18:01:27 -07001638
Allen Li0716efa2016-12-08 13:51:31 -08001639 @param options: SimpleNamespace from argument parsing.
1640
1641 @return: dict mapping version label prefixes to build names
Prashanth B6285f6a2014-05-08 18:01:27 -07001642 """
Dan Shi36cfd832014-10-10 13:38:51 -07001643 builds = {}
Rohit Makasanadf0a3a32017-06-30 13:55:18 -07001644 build_prefix = None
Dan Shi36cfd832014-10-10 13:38:51 -07001645 if options.build:
Rohit Makasanadf0a3a32017-06-30 13:55:18 -07001646 build_prefix = provision.get_version_label_prefix(options.build)
1647 builds[build_prefix] = options.build
1648 if options.cheets_build:
1649 builds[provision.CROS_ANDROID_VERSION_PREFIX] = options.cheets_build
1650 if build_prefix == provision.CROS_VERSION_PREFIX:
1651 builds[build_prefix] += provision.CHEETS_SUFFIX
Dan Shi0723bf52015-06-24 10:52:38 -07001652 if options.firmware_rw_build:
1653 builds[provision.FW_RW_VERSION_PREFIX] = options.firmware_rw_build
Dan Shi36cfd832014-10-10 13:38:51 -07001654 if options.firmware_ro_build:
1655 builds[provision.FW_RO_VERSION_PREFIX] = options.firmware_ro_build
Allen Li0716efa2016-12-08 13:51:31 -08001656 return builds
1657
1658
Prathmesh Prabhu9b8e7ad2017-10-30 14:26:13 -07001659def _make_child_deps_from_options(options):
1660 """Creates a list of extra dependencies for child jobs.
1661
1662 @param options: Parsed arguments to run_suite.
1663
1664 @returns: A list of label strings if any dependencies should be added. None
1665 otherwise.
1666 """
1667 if not options.model:
1668 return ()
1669 return ['model:%s' % options.model]
1670
1671
Allen Li0716efa2016-12-08 13:51:31 -08001672@retry.retry(error.StageControlFileFailure, timeout_min=10)
1673def create_suite(afe, options):
1674 """Create a suite with retries.
1675
1676 @param afe: The afe object to insert the new suite job into.
1677 @param options: The options to use in creating the suite.
1678
1679 @return: The afe_job_id of the new suite job.
1680 """
Prashanth B6285f6a2014-05-08 18:01:27 -07001681 logging.info('%s Submitted create_suite_job rpc',
1682 diagnosis_utils.JobTimer.format_time(datetime.now()))
Allen Li02b46c52017-09-11 11:48:12 -07001683
1684 # TODO(crbug.com/763207): This is to support calling old moblab RPC
1685 # with ToT code. This does not need to be supported after M62.
1686 if options.oldrpc:
1687 suite_args = options.suite_args
1688 if 'tests' in suite_args:
1689 # This is for test_that_wrapper
1690 suite_args = ' '.join([':lab:'] + suite_args['tests'])
1691 else:
1692 # This is for suite_attr_wrapper
1693 suite_args = repr(suite_args)
1694 options.suite_args = suite_args
1695
Allen Li53121702016-12-08 12:50:22 -08001696 return afe.run(
1697 'create_suite_job',
1698 name=options.name,
1699 board=options.board,
Allen Li0716efa2016-12-08 13:51:31 -08001700 builds=_make_builds_from_options(options),
Allen Li53121702016-12-08 12:50:22 -08001701 test_source_build=options.test_source_build,
Allen Li0fd08892016-12-08 13:47:38 -08001702 check_hosts=not options.no_wait,
Allen Li53121702016-12-08 12:50:22 -08001703 pool=options.pool,
Allen Lid3758d42016-12-08 13:46:17 -08001704 file_bugs=options.file_bugs,
Allen Li603728a2016-12-08 13:58:11 -08001705 priority=options.priority,
Allen Li53121702016-12-08 12:50:22 -08001706 suite_args=options.suite_args,
Allen Li0fd08892016-12-08 13:47:38 -08001707 wait_for_results=not options.no_wait,
Allen Li53121702016-12-08 12:50:22 -08001708 timeout_mins=options.timeout_mins + options.delay_minutes,
1709 max_runtime_mins=options.max_runtime_mins + options.delay_minutes,
1710 job_retry=options.retry,
1711 max_retries=options.max_retries,
1712 suite_min_duts=options.suite_min_duts,
Allen Li40599a32016-12-08 13:23:35 -08001713 offload_failures_only=options.offload_failures_only,
Allen Li53121702016-12-08 12:50:22 -08001714 run_prod_code=options.run_prod_code,
1715 delay_minutes=options.delay_minutes,
Shuqian Zhao843ae5c72017-02-22 11:25:01 -08001716 job_keyvals=options.job_keyvals,
Shuqian Zhaoed0da862017-03-06 14:47:13 -08001717 test_args=options.test_args,
Prathmesh Prabhu9b8e7ad2017-10-30 14:26:13 -07001718 child_dependencies=_make_child_deps_from_options(options),
Allen Li53121702016-12-08 12:50:22 -08001719 )
Prashanth B6285f6a2014-05-08 18:01:27 -07001720
1721
Allen Li85ae5df2017-07-10 14:58:16 -07001722class SuiteResult(namedtuple('SuiteResult', ['return_code', 'output_dict'])):
1723 """Result of running a suite to return."""
1724
Allen Licc205492017-07-10 17:26:04 -07001725 def __new__(cls, return_code, output_dict=None):
1726 if output_dict is None:
1727 output_dict = dict()
1728 else:
1729 output_dict = output_dict.copy()
1730 output_dict['return_code'] = return_code
Allen Li85ae5df2017-07-10 14:58:16 -07001731 return super(SuiteResult, cls).__new__(cls, return_code, output_dict)
Allen Li340414e2016-08-16 14:19:08 -07001732
1733
Allen Li5e9c35f2017-07-05 14:24:18 -07001734def _run_suite(options):
Aviv Keshet1480c4a2013-03-21 16:38:31 -07001735 """
Shuqian Zhao2fecacd2015-08-05 22:56:30 -07001736 run_suite script without exception handling.
Shuqian Zhaod2351072015-08-06 01:48:23 +00001737
Shuqian Zhao2fecacd2015-08-05 22:56:30 -07001738 @param options: The parsed options.
1739
1740 @returns: A tuple contains the return_code of run_suite and the dictionary
1741 of the output.
1742
1743 """
Shuqian Zhaoab1bedc2015-06-02 11:12:28 -07001744 # If indicate to use the new style suite control file, convert the args
1745 if options.use_suite_attr:
1746 options = change_options_for_suite_attr(options)
1747
xixuan99eba0b2017-07-12 15:10:01 -07001748 log_name = _get_log_name(options)
1749 utils.setup_logging(logfile=log_name)
Alex Miller88762a82013-09-04 15:41:28 -07001750
John Carey1425d292016-09-30 15:25:09 -07001751 if not options.bypass_labstatus and not options.web:
Fang Deng6197da32014-09-25 10:18:48 -07001752 utils.check_lab_status(options.build)
xixuanae791b12017-06-29 15:40:19 -07001753
1754 afe = _create_afe(options)
1755 instance_server = afe.server
Chris Masone359c0fd2012-03-13 15:18:59 -07001756
Dan Shi20952c12014-05-14 17:07:38 -07001757 rpc_helper = diagnosis_utils.RPCHelper(afe)
Fang Deng6865aab2015-02-20 14:49:47 -08001758 is_real_time = True
Chris Masone986459e2012-04-11 11:36:48 -07001759 if options.mock_job_id:
1760 job_id = int(options.mock_job_id)
Fang Deng6865aab2015-02-20 14:49:47 -08001761 existing_job = afe.get_jobs(id=job_id, finished=True)
1762 if existing_job:
1763 is_real_time = False
1764 else:
1765 existing_job = afe.get_jobs(id=job_id)
1766 if existing_job:
1767 job_created_on = time_utils.date_string_to_epoch_time(
1768 existing_job[0].created_on)
1769 else:
1770 raise utils.TestLabException('Failed to retrieve job: %d' % job_id)
Chris Masone986459e2012-04-11 11:36:48 -07001771 else:
Fang Deng5a43be62014-05-07 17:17:04 -07001772 try:
Fang Deng6865aab2015-02-20 14:49:47 -08001773 rpc_helper.check_dut_availability(options.board, options.pool,
Ningning Xiaf2c206c2016-04-13 14:15:51 -07001774 options.minimum_duts,
1775 options.skip_duts_check)
Prashanth B6285f6a2014-05-08 18:01:27 -07001776 job_id = create_suite(afe, options)
Fang Deng6865aab2015-02-20 14:49:47 -08001777 job_created_on = time.time()
Fang Deng5a43be62014-05-07 17:17:04 -07001778 except (error.CrosDynamicSuiteException,
1779 error.RPCException, proxy.JSONRPCException) as e:
Allen Lic3aa7692016-08-08 11:45:00 -07001780 logging.exception('Error Message: %s', e)
Allen Li096e1852017-07-14 09:46:07 -07001781 return SuiteResult(RETURN_CODES.INFRA_FAILURE,
1782 {'return_message': str(e)})
Xixuan Wu610606b2017-11-13 14:03:33 -08001783 except AttributeError as e:
1784 logging.exception('Error Message: %s', e)
Allen Licc205492017-07-10 17:26:04 -07001785 return SuiteResult(RETURN_CODES.INVALID_OPTIONS)
Fang Deng5a43be62014-05-07 17:17:04 -07001786
Prashanth B923ca262014-03-14 12:36:29 -07001787 job_timer = diagnosis_utils.JobTimer(
Fang Deng6865aab2015-02-20 14:49:47 -08001788 job_created_on, float(options.timeout_mins))
Aviv Keshet9afee5e2014-10-09 16:33:09 -07001789 job_url = reporting_utils.link_job(job_id,
1790 instance_server=instance_server)
Prashanth B923ca262014-03-14 12:36:29 -07001791 logging.info('%s Created suite job: %s',
1792 job_timer.format_time(job_timer.job_created_time),
Aviv Keshet9afee5e2014-10-09 16:33:09 -07001793 job_url)
Allen Lie082ced2016-09-14 15:19:20 -07001794 logging.info(annotations.StepLink(
1795 text='Link to suite',
1796 url=job_url))
Aviv Keshetdb321de2015-04-10 19:09:58 -07001797
1798 if options.create_and_return:
Shuqian Zhao2fecacd2015-08-05 22:56:30 -07001799 msg = '--create_and_return was specified, terminating now.'
1800 logging.info(msg)
Prathmesh Prabhud9d23802017-08-22 00:46:01 +00001801 return SuiteResult(RETURN_CODES.OK, {'return_message': msg})
Aviv Keshetdb321de2015-04-10 19:09:58 -07001802
Allen Li93f4db52016-09-14 14:44:59 -07001803 if options.no_wait:
Allen Li340414e2016-08-16 14:19:08 -07001804 return _handle_job_nowait(job_id, options, instance_server)
Allen Li93f4db52016-09-14 14:44:59 -07001805 else:
1806 return _handle_job_wait(afe, job_id, options, job_timer, is_real_time)
Allen Li340414e2016-08-16 14:19:08 -07001807
1808
xixuan99eba0b2017-07-12 15:10:01 -07001809def _get_log_name(options):
1810 """Return local log file's name.
1811
1812 @param options: Parsed options.
1813
1814 @return log_name, a string file name.
1815 """
1816 if options.require_logfile:
1817 # options.build is verified to exist in verify_options.
1818 # convert build name from containing / to containing only _.
1819 log_name = 'run_suite-%s.log' % options.build.replace('/', '_')
1820 log_dir = os.path.join(common.autotest_dir, 'logs')
1821 if os.path.exists(log_dir):
1822 log_name = os.path.join(log_dir, log_name)
1823
1824 return log_name
1825 else:
1826 return None
1827
1828
xixuanae791b12017-06-29 15:40:19 -07001829def _create_afe(options):
1830 """Return an afe instance based on options.
1831
1832 @param options Parsed options.
1833
1834 @return afe, an AFE instance.
1835 """
1836 instance_server = (options.web if options.web else
1837 instance_for_pool(options.pool))
1838 afe = frontend_wrappers.RetryingAFE(server=instance_server,
1839 timeout_min=options.afe_timeout_mins,
1840 delay_sec=options.delay_sec)
1841 logging.info('Autotest instance created: %s', instance_server)
1842 return afe
1843
1844
Allen Li340414e2016-08-16 14:19:08 -07001845def _handle_job_wait(afe, job_id, options, job_timer, is_real_time):
1846 """Handle suite job synchronously.
1847
1848 @param afe AFE instance.
1849 @param job_id Suite job id.
1850 @param options Parsed options.
1851 @param job_timer JobTimer for suite job.
1852 @param is_real_time Whether or not to handle job timeout.
1853
1854 @return SuiteResult of suite job.
1855 """
Allen Li340414e2016-08-16 14:19:08 -07001856 rpc_helper = diagnosis_utils.RPCHelper(afe)
1857 instance_server = afe.server
1858 while not afe.get_jobs(id=job_id, finished=True):
Allen Li425d91f2017-07-10 15:14:20 -07001859 _poke_buildbot_with_output(afe, job_id, job_timer)
Allen Li340414e2016-08-16 14:19:08 -07001860 if job_timer.debug_output_timer.poll():
1861 logging.info('The suite job has another %s till timeout.',
Allen Li425d91f2017-07-10 15:14:20 -07001862 job_timer.timeout_hours - job_timer.elapsed_time())
Allen Li340414e2016-08-16 14:19:08 -07001863 time.sleep(10)
xixuana96bd212017-01-13 12:51:22 +08001864 logging.info('%s Suite job is finished.',
1865 diagnosis_utils.JobTimer.format_time(datetime.now()))
Allen Li340414e2016-08-16 14:19:08 -07001866 # For most cases, ResultCollector should be able to determine whether
1867 # a suite has timed out by checking information in the test view.
1868 # However, occationally tko parser may fail on parsing the
1869 # job_finished time from the job's keyval file. So we add another
1870 # layer of timeout check in run_suite. We do the check right after
1871 # the suite finishes to make it as accurate as possible.
1872 # There is a minor race condition here where we might have aborted
1873 # for some reason other than a timeout, and the job_timer thinks
1874 # it's a timeout because of the jitter in waiting for results.
1875 # The consequence would be that run_suite exits with code
1876 # SUITE_TIMEOUT while it should have returned INFRA_FAILURE
1877 # instead, which should happen very rarely.
1878 # Note the timeout will have no sense when using -m option.
1879 is_suite_timeout = job_timer.is_suite_timeout()
1880
1881 # Extract the original suite name to record timing.
1882 original_suite_name = get_original_suite_name(options.name,
Allen Li425d91f2017-07-10 15:14:20 -07001883 options.suite_args)
Allen Li340414e2016-08-16 14:19:08 -07001884 # Start collecting test results.
Aseda Aboagyed72df752017-05-22 14:30:11 -07001885 logging.info('%s Start collecting test results and dump them to json.',
xixuana96bd212017-01-13 12:51:22 +08001886 diagnosis_utils.JobTimer.format_time(datetime.now()))
Alex Millerc7a59522013-10-30 15:18:57 -07001887 TKO = frontend_wrappers.RetryingTKO(server=instance_server,
Simran Basi25effe32013-11-26 13:02:11 -08001888 timeout_min=options.afe_timeout_mins,
Chris Masone8ac66712012-02-15 14:21:02 -08001889 delay_sec=options.delay_sec)
Allen Li637683b2017-11-06 17:36:27 -08001890 # TODO(crbug.com/672348): It needs to be possible for provision
1891 # suite to pass if only a few tests fail. Otherwise, a single
1892 # failing test will be reported as failure even if the suite reports
1893 # success.
Allen Li977760b2017-11-06 18:11:37 -08001894 if options.name == _PROVISION_SUITE:
Allen Li637683b2017-11-06 17:36:27 -08001895 # TODO(crbug.com/672348): Creating the suite job requires that
1896 # suite_args contains num_required.
Allen Li422f24b2017-07-12 15:15:43 -07001897 return_code_function = _ProvisionReturnCodeComputer(
1898 num_required=options.suite_args['num_required'])
1899 else:
1900 return_code_function = _ReturnCodeComputer()
Allen Li340414e2016-08-16 14:19:08 -07001901 collector = ResultCollector(instance_server=instance_server,
1902 afe=afe, tko=TKO, build=options.build,
1903 board=options.board,
1904 suite_name=options.name,
1905 suite_job_id=job_id,
Allen Li422f24b2017-07-12 15:15:43 -07001906 return_code_function=return_code_function,
Allen Li340414e2016-08-16 14:19:08 -07001907 original_suite_name=original_suite_name)
1908 collector.run()
1909 # Dump test outputs into json.
1910 output_dict = collector.get_results_dict()
1911 output_dict['autotest_instance'] = instance_server
1912 if not options.json_dump:
1913 collector.output_results()
Allen Licc205492017-07-10 17:26:04 -07001914 result = collector.return_result
Allen Li340414e2016-08-16 14:19:08 -07001915 if is_real_time:
1916 # Do not record stats if the suite was aborted (either by a user
1917 # or through the golo rpc).
1918 # Also do not record stats if is_aborted is None, indicating
1919 # aborting status is unknown yet.
1920 if collector.is_aborted == False:
xixuana96bd212017-01-13 12:51:22 +08001921 logging.info('%s Gathering timing stats for the suite job.',
1922 diagnosis_utils.JobTimer.format_time(datetime.now()))
Allen Li340414e2016-08-16 14:19:08 -07001923 collector.gather_timing_stats()
J. Richard Barnette712eb402013-08-13 18:03:00 -07001924
Allen Li340414e2016-08-16 14:19:08 -07001925 if collector.is_aborted == True and is_suite_timeout:
1926 # There are two possible cases when a suite times out.
1927 # 1. the suite job was aborted due to timing out
1928 # 2. the suite job succeeded, but some child jobs
1929 # were already aborted before the suite job exited.
1930 # The case 2 was handled by ResultCollector,
1931 # here we handle case 1.
Allen Licc205492017-07-10 17:26:04 -07001932 result |= _RETURN_RESULTS['suite_timeout']
xixuana96bd212017-01-13 12:51:22 +08001933 logging.info('\n %s Attempting to display pool info: %s',
1934 diagnosis_utils.JobTimer.format_time(datetime.now()),
1935 options.pool)
Allen Li340414e2016-08-16 14:19:08 -07001936 try:
1937 # Add some jitter to make up for any latency in
1938 # aborting the suite or checking for results.
Allen Li0b675b62017-07-05 13:38:04 -07001939 cutoff = job_timer.timeout_hours + timedelta(hours=0.3)
Allen Li340414e2016-08-16 14:19:08 -07001940 rpc_helper.diagnose_pool(
1941 options.board, options.pool, cutoff)
Allen Lid4aa2fb2016-12-08 14:03:54 -08001942 except proxy.JSONRPCException:
Allen Li340414e2016-08-16 14:19:08 -07001943 logging.warning('Unable to display pool info.')
Aviv Keshet6b1122d2016-06-20 13:29:52 -07001944
Allen Li340414e2016-08-16 14:19:08 -07001945 # And output return message.
Allen Licc205492017-07-10 17:26:04 -07001946 if result.message:
1947 logging.info('Reason: %s', result.message)
Fang Deng5a43be62014-05-07 17:17:04 -07001948
xixuana96bd212017-01-13 12:51:22 +08001949 logging.info('\n %s Output below this line is for buildbot consumption:',
1950 diagnosis_utils.JobTimer.format_time(datetime.now()))
Allen Li28be0642017-07-10 15:16:26 -07001951 log_buildbot_links(logging.info, collector.buildbot_links)
Allen Licc205492017-07-10 17:26:04 -07001952 return result.suite_result(output_dict)
Prashanth B923ca262014-03-14 12:36:29 -07001953
Allen Li340414e2016-08-16 14:19:08 -07001954
1955def _handle_job_nowait(job_id, options, instance_server):
1956 """Handle suite job asynchronously.
1957
1958 @param job_id Suite job id.
1959 @param options Parsed options.
1960 @param instance_server Autotest instance hostname.
1961
1962 @return SuiteResult of suite job.
1963 """
1964 logging.info('Created suite job: %r', job_id)
1965 link = LogLink(options.name, instance_server,
Allen Li0b675b62017-07-05 13:38:04 -07001966 '%s-%s' % (job_id, getpass.getuser()))
Allen Li340414e2016-08-16 14:19:08 -07001967 for generate_link in link.GenerateBuildbotLinks():
1968 logging.info(generate_link)
1969 logging.info('--no_wait specified; Exiting.')
1970 return SuiteResult(RETURN_CODES.OK,
Allen Li0b675b62017-07-05 13:38:04 -07001971 {'return_message': '--no_wait specified; Exiting.'})
Chris Masone24b80f12012-02-14 14:18:01 -08001972
Fang Dengdd20e452014-04-07 15:39:47 -07001973
xixuanae791b12017-06-29 15:40:19 -07001974def _should_run(options):
1975 """Check whether the suite should be run based on lab/job status checking.
1976
1977 @param options Parsed options.
1978 """
1979 try:
1980 site_utils.check_lab_status(options.test_source_build)
1981 except site_utils.TestLabException as ex:
1982 logging.exception('Lab is closed or build is blocked. Skipping '
1983 'suite %s, board %s, build %s: %s',
1984 options.name, options.board,
1985 options.test_source_build, str(ex))
1986 return False
1987
1988 start_time = str(datetime.now() -
1989 timedelta(days=_SEARCH_JOB_MAX_DAYS))
1990 afe = _create_afe(options)
Xixuan Wuf8ca7822017-09-07 17:28:09 -07001991 afe_job_id = afe.get_jobs(
xixuanae791b12017-06-29 15:40:19 -07001992 name__istartswith=options.test_source_build,
1993 name__iendswith='control.'+options.name,
1994 created_on__gte=start_time,
1995 min_rpc_timeout=_MIN_RPC_TIMEOUT)
Xixuan Wuf8ca7822017-09-07 17:28:09 -07001996 if afe_job_id:
1997 logging.info('Found duplicate suite %s scheduled in past.',
1998 afe_job_id)
1999 return False
xixuanae791b12017-06-29 15:40:19 -07002000
Xixuan Wu3ae6e8b2017-10-12 09:57:07 -07002001 return True
2002
Shuqian Zhao2fecacd2015-08-05 22:56:30 -07002003
Allen Li425d91f2017-07-10 15:14:20 -07002004def _poke_buildbot_with_output(afe, job_id, job_timer):
2005 """Poke buildbot so it doesn't timeout from silence.
2006
2007 @param afe AFE instance.
2008 @param job_id Suite job id.
2009 @param job_timer JobTimer for suite job.
2010 """
2011 rpc_helper = diagnosis_utils.RPCHelper(afe)
2012 # Note that this call logs output, preventing buildbot's
2013 # 9000 second silent timeout from kicking in. Let there be no
2014 # doubt, this is a hack. The timeout is from upstream buildbot and
2015 # this is the easiest work around.
2016 if job_timer.first_past_halftime():
2017 rpc_helper.diagnose_job(job_id, afe.server)
2018
2019
2020
Allen Li85ae5df2017-07-10 14:58:16 -07002021def _run_task(options):
Allen Li5e9c35f2017-07-05 14:24:18 -07002022 """Perform this script's function minus setup.
Aviv Keshet97bebd42017-05-24 21:02:32 -07002023
Allen Li5e9c35f2017-07-05 14:24:18 -07002024 Boilerplate like argument parsing, logging, output formatting happen
2025 elsewhere.
Allen Li85ae5df2017-07-10 14:58:16 -07002026
2027 Returns a SuiteResult instance.
2028
2029 TODO(ayatane): The try/except should be moved into _run_suite().
2030 Good luck trying to figure out which function calls are supposed to
2031 raise which of the exceptions.
Allen Li5e9c35f2017-07-05 14:24:18 -07002032 """
Fang Dengfb4a9492014-09-18 17:52:06 -07002033 try:
Allen Li85ae5df2017-07-10 14:58:16 -07002034 return _run_suite(options)
Shuqian Zhaoade6e7d2015-12-07 18:01:11 -08002035 except diagnosis_utils.BoardNotAvailableError as e:
Allen Li85ae5df2017-07-10 14:58:16 -07002036 result = SuiteResult(
2037 RETURN_CODES.BOARD_NOT_AVAILABLE,
2038 {'return_message': 'Skipping testing: %s' % e.message})
2039 logging.info(result.output_dict['return_message'])
2040 return result
Shuqian Zhaoade6e7d2015-12-07 18:01:11 -08002041 except utils.TestLabException as e:
Allen Li85ae5df2017-07-10 14:58:16 -07002042 result = SuiteResult(
2043 RETURN_CODES.INFRA_FAILURE,
2044 {'return_message': 'TestLabException: %s' % e})
2045 logging.exception(result.output_dict['return_message'])
2046 return result
2047
2048
2049class _ExceptionHandler(object):
2050 """Global exception handler replacement."""
2051
2052 def __init__(self, dump_json):
2053 """Initialize instance.
2054
2055 @param dump_json: Whether to print a JSON dump of the result dict to
2056 stdout.
2057 """
2058 self._should_dump_json = dump_json
2059
2060 def __call__(self, exc_type, value, traceback):
2061 if self._should_dump_json:
2062 _dump_json({'return_message': ('Unhandled run_suite exception: %s'
2063 % value)})
2064 sys.exit(RETURN_CODES.INFRA_FAILURE)
Allen Li5e9c35f2017-07-05 14:24:18 -07002065
2066
2067def main():
2068 """Entry point."""
2069 utils.verify_not_root_user()
2070
2071 parser = make_parser()
2072 options = parser.parse_args()
2073 if options.do_nothing:
Allen Li85ae5df2017-07-10 14:58:16 -07002074 return 0
2075
2076 sys.exceptionhandler = _ExceptionHandler(dump_json=options.json_dump)
Allen Li5e9c35f2017-07-05 14:24:18 -07002077 if options.json_dump:
2078 logging.disable(logging.CRITICAL)
2079
Allen Li85ae5df2017-07-10 14:58:16 -07002080 options_okay = verify_and_clean_options(options)
Xixuan Wuf8ca7822017-09-07 17:28:09 -07002081 # Set StreamHandler first to capture error messages if suite is not run.
2082 utils.setup_logging()
Allen Li85ae5df2017-07-10 14:58:16 -07002083 if not options_okay:
2084 parser.print_help()
Allen Licc205492017-07-10 17:26:04 -07002085 result = SuiteResult(RETURN_CODES.INVALID_OPTIONS)
Allen Li85ae5df2017-07-10 14:58:16 -07002086 elif options.pre_check and not _should_run(options):
Xixuan Wuf8ca7822017-09-07 17:28:09 -07002087 logging.info('Suite %s-%s is terminated: Lab is closed, OR build is '
2088 'blocked, OR this suite has already been kicked off '
Allen Li85ae5df2017-07-10 14:58:16 -07002089 'once in past %d days.',
2090 options.test_source_build, options.name,
2091 _SEARCH_JOB_MAX_DAYS)
2092 result = SuiteResult(
2093 RETURN_CODES.ERROR,
2094 {'return_message': ("Lab is closed OR other reason"
2095 " (see code, it's complicated)")})
2096 else:
2097 result = _run_task(options)
Shuqian Zhao2fecacd2015-08-05 22:56:30 -07002098
Shuqian Zhao2fecacd2015-08-05 22:56:30 -07002099 if options.json_dump:
Allen Li85ae5df2017-07-10 14:58:16 -07002100 _dump_json(result.output_dict)
Fang Deng6197da32014-09-25 10:18:48 -07002101
2102 logging.info('Will return from run_suite with status: %s',
Allen Li85ae5df2017-07-10 14:58:16 -07002103 RETURN_CODES.get_string(result.return_code))
2104 return result.return_code
2105
2106
2107def _dump_json(obj):
2108 """Write obj JSON to stdout."""
2109 output_json = json.dumps(obj, sort_keys=True)
Prathmesh Prabhud9d23802017-08-22 00:46:01 +00002110 sys.stdout.write('#JSON_START#%s#JSON_END#' % output_json.strip())
Fang Dengfb4a9492014-09-18 17:52:06 -07002111
2112
Chris Masone24b80f12012-02-14 14:18:01 -08002113if __name__ == "__main__":
2114 sys.exit(main())