Prathmesh Prabhu | dd9071f | 2018-07-25 12:36:07 -0700 | [diff] [blame] | 1 | # Copyright 2018 The Chromium Authors. All rights reserved. |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
| 5 | """Run the skylab staging test. |
| 6 | |
| 7 | This script runs a suite of autotest tests and some other sanity checks against |
| 8 | a given Skylab instance. If all sanity checks and tests have the expected |
| 9 | results, the script exits with success. |
| 10 | |
| 11 | This script is intended to be used for the Autotest staging lab's test_push. |
| 12 | This script does not update any software before running the tests (i.e. caller |
| 13 | is responsible for setting up the staging lab with the correct software |
| 14 | beforehand), nor does it update any software refs on success (i.e. caller is |
| 15 | responsible for blessing the newer version of software as needed). |
| 16 | """ |
| 17 | |
| 18 | from __future__ import absolute_import |
| 19 | from __future__ import division |
| 20 | from __future__ import print_function |
| 21 | |
| 22 | import argparse |
Alex Zamorzaev | f0573b5 | 2019-04-05 12:07:59 -0700 | [diff] [blame] | 23 | import collections |
Prathmesh Prabhu | dd9071f | 2018-07-25 12:36:07 -0700 | [diff] [blame] | 24 | import logging |
| 25 | import sys |
| 26 | import time |
| 27 | |
| 28 | from lucifer import autotest |
| 29 | from lucifer import loglib |
| 30 | from skylab_staging import errors |
| 31 | from skylab_staging import swarming |
| 32 | |
| 33 | _METRICS_PREFIX = 'chromeos/autotest/test_push/skylab' |
| 34 | _POLLING_INTERVAL_S = 10 |
| 35 | _WAIT_FOR_DUTS_TIMEOUT_S = 20 * 60 |
| 36 | |
Xixuan Wu | 51f41d8 | 2018-08-10 15:19:07 -0700 | [diff] [blame] | 37 | # Dictionary of test results expected in suite:skylab_staging_test. |
Alex Zamorzaev | f0573b5 | 2019-04-05 12:07:59 -0700 | [diff] [blame] | 38 | _EXPECTED_TEST_RESULTS = {'login_LoginSuccess.*': ['GOOD'], |
| 39 | 'provision_AutoUpdate.double': ['GOOD'], |
Alex Zamorzaev | 9033aca | 2019-04-18 14:08:19 -0700 | [diff] [blame] | 40 | 'dummy_Pass$': ['GOOD'], |
| 41 | 'dummy_Pass.actionable$': ['GOOD'], |
Alex Zamorzaev | 0891e4e | 2019-04-23 16:42:19 -0700 | [diff] [blame^] | 42 | 'dummy_Pass.bluetooth$': ['GOOD'], |
Alex Zamorzaev | 9033aca | 2019-04-18 14:08:19 -0700 | [diff] [blame] | 43 | # ssp and nossp. |
| 44 | 'dummy_PassServer$': ['GOOD', 'GOOD'], |
| 45 | # The entire dummy_Fail test is retried. |
Alex Zamorzaev | 0756d17 | 2019-04-15 17:29:35 -0700 | [diff] [blame] | 46 | 'dummy_Fail.Fail$': ['FAIL', 'FAIL'], |
| 47 | 'dummy_Fail.Error$': ['ERROR', 'ERROR'], |
| 48 | 'dummy_Fail.Warn$': ['WARN', 'WARN'], |
| 49 | 'dummy_Fail.NAError$': ['TEST_NA', |
| 50 | 'TEST_NA'], |
| 51 | 'dummy_Fail.Crash$': ['GOOD', 'GOOD'], |
Alex Zamorzaev | 9033aca | 2019-04-18 14:08:19 -0700 | [diff] [blame] | 52 | 'tast.*': ['GOOD'], |
Alex Zamorzaev | f0573b5 | 2019-04-05 12:07:59 -0700 | [diff] [blame] | 53 | } |
Xixuan Wu | 51f41d8 | 2018-08-10 15:19:07 -0700 | [diff] [blame] | 54 | |
| 55 | # Some test could be missing from the test results for various reasons. Add |
| 56 | # such test in this list and explain the reason. |
| 57 | _IGNORED_TESTS = [ |
Xixuan Wu | 51f41d8 | 2018-08-10 15:19:07 -0700 | [diff] [blame] | 58 | ] |
Prathmesh Prabhu | dd9071f | 2018-07-25 12:36:07 -0700 | [diff] [blame] | 59 | |
| 60 | _logger = logging.getLogger(__name__) |
| 61 | |
| 62 | |
Prathmesh Prabhu | dd9071f | 2018-07-25 12:36:07 -0700 | [diff] [blame] | 63 | def main(): |
| 64 | """Entry point of test_push.""" |
| 65 | autotest.monkeypatch() |
| 66 | metrics = autotest.chromite_load('metrics') |
| 67 | ts_mon_config = autotest.chromite_load('ts_mon_config') |
| 68 | |
| 69 | parser = _get_parser() |
| 70 | loglib.add_logging_options(parser) |
| 71 | args = parser.parse_args() |
| 72 | loglib.configure_logging_with_args(parser, args) |
| 73 | |
| 74 | with ts_mon_config.SetupTsMonGlobalState(service_name='skylab_test_push', |
| 75 | indirect=True): |
| 76 | success = False |
| 77 | try: |
| 78 | with metrics.SecondsTimer(_METRICS_PREFIX + '/durations/total', |
| 79 | add_exception_field=True): |
| 80 | _run_test_push(args) |
| 81 | success = True |
| 82 | finally: |
| 83 | metrics.Counter(_METRICS_PREFIX + '/tick').increment( |
| 84 | fields={'success': success}) |
| 85 | |
| 86 | def _get_parser(): |
| 87 | parser = argparse.ArgumentParser( |
| 88 | description='Run test_push against Skylab instance.') |
| 89 | parser.add_argument( |
| 90 | '--swarming-url', |
| 91 | required=True, |
| 92 | help='Full URL to the Swarming instance to use', |
| 93 | ) |
| 94 | parser.add_argument( |
| 95 | '--swarming-cli', |
| 96 | required=True, |
| 97 | help='Path to the Swarming cli tool.', |
| 98 | ) |
| 99 | # TODO(crbug.com/867969) Use model instead of board once skylab inventory has |
| 100 | # model information. |
| 101 | parser.add_argument( |
| 102 | '--dut-board', |
| 103 | required=True, |
| 104 | help='Label board of the DUTs to use for testing', |
| 105 | ) |
| 106 | parser.add_argument( |
| 107 | '--dut-pool', |
| 108 | required=True, |
| 109 | choices=('DUT_POOL_CQ', 'DUT_POOL_BVT', 'DUT_POOL_SUITES'), |
| 110 | help='Label pool of the DUTs to use for testing', |
| 111 | ) |
| 112 | parser.add_argument( |
| 113 | '--build', |
| 114 | required=True, |
| 115 | help='ChromeOS build to use for provisioning' |
| 116 | ' (e.g.: gandolf-release/R54-8743.25.0).', |
| 117 | ) |
| 118 | parser.add_argument( |
| 119 | '--timeout-mins', |
| 120 | type=int, |
| 121 | required=True, |
| 122 | help='(Optional) Overall timeout for the test_push. On timeout, test_push' |
| 123 | ' attempts to abort any in-flight test suites before quitting.', |
| 124 | ) |
| 125 | parser.add_argument( |
| 126 | '--num-min-duts', |
| 127 | type=int, |
| 128 | help='Minimum number of Ready DUTs required for test suite.', |
| 129 | ) |
| 130 | parser.add_argument( |
| 131 | '--service-account-json', |
| 132 | default=None, |
| 133 | help='(Optional) Path to the service account credentials file to' |
| 134 | ' authenticate with Swarming service.', |
| 135 | ) |
| 136 | return parser |
| 137 | |
| 138 | |
| 139 | def _run_test_push(args): |
| 140 | """Meat of the test_push flow.""" |
| 141 | metrics = autotest.chromite_load('metrics') |
| 142 | |
| 143 | deadline = time.time() + (args.timeout_mins * 60) |
| 144 | swclient = swarming.Client(args.swarming_cli, args.swarming_url, |
| 145 | args.service_account_json) |
| 146 | if args.num_min_duts: |
| 147 | _ensure_duts_ready( |
| 148 | swclient, |
| 149 | args.dut_board, |
| 150 | args.dut_pool, |
| 151 | args.num_min_duts, |
| 152 | min(deadline - time.time(), _WAIT_FOR_DUTS_TIMEOUT_S), |
| 153 | ) |
| 154 | |
| 155 | # Just like the builders, first run a provision suite to provision required |
| 156 | # DUTs, then run the actual suite. |
| 157 | with metrics.SecondsTimer(_METRICS_PREFIX + '/durations/provision_suite', |
| 158 | add_exception_field=True): |
| 159 | task_id = swclient.trigger_suite( |
| 160 | args.dut_board, |
| 161 | args.dut_pool, |
| 162 | args.build, |
| 163 | 'provision', |
| 164 | deadline - time.time(), |
| 165 | ) |
| 166 | _logger.info('Triggered provision suite. Task id: %s', task_id) |
| 167 | swclient.wait_for_suite( |
| 168 | task_id, |
| 169 | args.dut_board, |
| 170 | args.dut_pool, |
| 171 | args.build, |
| 172 | 'provision', |
| 173 | deadline - time.time(), |
| 174 | ) |
| 175 | _logger.info('Finished provision suite.') |
| 176 | |
| 177 | with metrics.SecondsTimer(_METRICS_PREFIX + '/durations/push_to_prod_suite', |
| 178 | add_exception_field=True): |
| 179 | task_id = swclient.trigger_suite( |
| 180 | args.dut_board, |
| 181 | args.dut_pool, |
| 182 | args.build, |
Prathmesh Prabhu | 8bd1892 | 2018-08-16 11:08:04 -0700 | [diff] [blame] | 183 | 'skylab_staging_test', |
Prathmesh Prabhu | dd9071f | 2018-07-25 12:36:07 -0700 | [diff] [blame] | 184 | deadline - time.time(), |
| 185 | ) |
Prathmesh Prabhu | 8bd1892 | 2018-08-16 11:08:04 -0700 | [diff] [blame] | 186 | _logger.info('Triggered skylab_staging_test suite. Task id: %s', task_id) |
Xixuan Wu | 5dcca50 | 2019-01-07 12:24:37 -0800 | [diff] [blame] | 187 | _verify_suite_creation(swclient, task_id) |
Xixuan Wu | 51f41d8 | 2018-08-10 15:19:07 -0700 | [diff] [blame] | 188 | _logger.info('Check push_to_prod suite on: \n %s', |
| 189 | swclient.task_url(task_id)) |
Prathmesh Prabhu | dd9071f | 2018-07-25 12:36:07 -0700 | [diff] [blame] | 190 | swclient.wait_for_suite( |
| 191 | task_id, |
| 192 | args.dut_board, |
| 193 | args.dut_pool, |
| 194 | args.build, |
Prathmesh Prabhu | 8bd1892 | 2018-08-16 11:08:04 -0700 | [diff] [blame] | 195 | 'skylab_staging_test', |
Prathmesh Prabhu | dd9071f | 2018-07-25 12:36:07 -0700 | [diff] [blame] | 196 | deadline - time.time(), |
| 197 | ) |
Prathmesh Prabhu | 8bd1892 | 2018-08-16 11:08:04 -0700 | [diff] [blame] | 198 | _logger.info('Finished skylab_staging_test suite.') |
Xixuan Wu | 51f41d8 | 2018-08-10 15:19:07 -0700 | [diff] [blame] | 199 | |
| 200 | _verify_test_results(task_id, _EXPECTED_TEST_RESULTS) |
| 201 | |
| 202 | |
Xixuan Wu | 5dcca50 | 2019-01-07 12:24:37 -0800 | [diff] [blame] | 203 | def _verify_suite_creation(swclient, task_id): |
| 204 | """Verify the suite is created successfully.""" |
| 205 | result = swclient.query('task/%s/result' % task_id, []) |
| 206 | if result['state'] != 'COMPLETED' or result['failure']: |
| 207 | raise errors.TestPushError('Suite task %s is not successfully created.' |
| 208 | % task_id) |
| 209 | |
| 210 | |
Xixuan Wu | 51f41d8 | 2018-08-10 15:19:07 -0700 | [diff] [blame] | 211 | def _verify_test_results(task_id, expected_results): |
| 212 | """Verify if test results are expected.""" |
| 213 | _logger.info('Comparing test results...') |
| 214 | test_views = _get_test_views(task_id) |
| 215 | available_views = [v for v in test_views if _view_is_preserved(v)] |
| 216 | logging.debug('Test results:') |
| 217 | for v in available_views: |
| 218 | logging.debug('%s%s', v['test_name'].ljust(30), v['status']) |
| 219 | |
| 220 | summary = _verify_and_summarize(available_views, expected_results) |
| 221 | if summary: |
| 222 | logging.error('\n'.join(summary)) |
| 223 | raise errors.TestPushError('Test results are not consistent with ' |
| 224 | 'expected results') |
| 225 | |
| 226 | |
| 227 | def _get_test_views(task_id): |
| 228 | """Retrieve test views from TKO for skylab task id.""" |
| 229 | tko_db = autotest.load('tko.db') |
| 230 | db = tko_db.db() |
| 231 | return db.get_child_tests_by_parent_task_id(task_id) |
| 232 | |
| 233 | |
| 234 | def _view_is_preserved(view): |
| 235 | """Detect whether to keep the test view for further comparison.""" |
| 236 | job_status = autotest.load('server.cros.dynamic_suite.job_status') |
| 237 | return (job_status.view_is_relevant(view) and |
| 238 | (not job_status.view_is_for_suite_job(view))) |
| 239 | |
| 240 | |
| 241 | def _verify_and_summarize(available_views, expected_results): |
| 242 | """Verify and generate summaries for test_push results.""" |
| 243 | test_push_common = autotest.load('site_utils.test_push_common') |
Alex Zamorzaev | f0573b5 | 2019-04-05 12:07:59 -0700 | [diff] [blame] | 244 | views = collections.defaultdict(list) |
| 245 | for view in available_views: |
| 246 | views[view['test_name']].append(view['status']) |
Xixuan Wu | 51f41d8 | 2018-08-10 15:19:07 -0700 | [diff] [blame] | 247 | return test_push_common.summarize_push(views, expected_results, |
| 248 | _IGNORED_TESTS) |
Prathmesh Prabhu | dd9071f | 2018-07-25 12:36:07 -0700 | [diff] [blame] | 249 | |
| 250 | |
| 251 | def _ensure_duts_ready(swclient, board, pool, min_duts, timeout_s): |
| 252 | """Ensure that at least num_duts are in the ready dut_state.""" |
| 253 | start_time = time.time() |
| 254 | while True: |
| 255 | _logger.debug('Checking whether %d DUTs are available', min_duts) |
| 256 | num_duts = swclient.num_ready_duts(board, pool) |
| 257 | if num_duts >= min_duts: |
| 258 | _logger.info( |
| 259 | '%d available DUTs satisfy the minimum requirement of %d DUTs', |
| 260 | num_duts, min_duts, |
| 261 | ) |
| 262 | return |
| 263 | if time.time() - start_time > timeout_s: |
| 264 | raise errors.TestPushError( |
| 265 | 'Could not find %d ready DUTs with (board:%s, pool:%s) within %d' |
| 266 | ' seconds' % (min_duts, board, pool, timeout_s) |
| 267 | ) |
| 268 | time.sleep(_POLLING_INTERVAL_S) |
| 269 | |
| 270 | |
| 271 | if __name__ == '__main__': |
| 272 | sys.exit(main()) |