blob: 3e8fa2f4dafdea10e2d6f3f1e434156acb0677c0 [file] [log] [blame]
Xixuan Wuc7bf77c2018-04-24 12:05:40 -07001# Copyright 2018 The Chromium OS 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"""Module for CrOS dynamic test suite generation and execution."""
6
7from __future__ import absolute_import
8from __future__ import division
9from __future__ import print_function
10
Xixuan Wu80795c82018-06-12 11:56:17 -070011import contextlib
12import itertools
Xixuan Wu0bea9522018-05-08 17:49:19 -070013import json
Xixuan Wu6c041332018-05-07 16:04:36 -070014import logging
Xixuan Wue71c8932018-05-07 17:18:34 -070015import os
Aviv Keshetf0951212019-03-18 14:54:32 -070016import re
Xixuan Wu0bea9522018-05-08 17:49:19 -070017import time
Xixuan Wu6c041332018-05-07 16:04:36 -070018
Xixuan Wue71c8932018-05-07 17:18:34 -070019from lucifer import autotest
Xixuan Wu9af95a22018-05-18 10:46:42 -070020from skylab_suite import cros_suite
Xixuan Wu0bea9522018-05-08 17:49:19 -070021from skylab_suite import swarming_lib
Xixuan Wuc7bf77c2018-04-24 12:05:40 -070022
Xixuan Wu0bea9522018-05-08 17:49:19 -070023
Allen Li1ccca8f2018-08-29 12:11:06 -070024SKYLAB_DRONE_SWARMING_WORKER = '/opt/infra-tools/skylab_swarming_worker'
Aviv Keshetf0951212019-03-18 14:54:32 -070025SKYLAB_SUITE_USER = 'skylab_suite_runner'
26SKYLAB_TOOL = '/opt/infra-tools/skylab'
Xixuan Wuc7bf77c2018-04-24 12:05:40 -070027
Xixuan Wu80795c82018-06-12 11:56:17 -070028SUITE_WAIT_SLEEP_INTERVAL_SECONDS = 30
29
Xixuan Wu79d14662018-08-20 11:15:41 -070030# See #5 in crbug.com/873886 for more details.
31_NOT_SUPPORTED_DEPENDENCIES = ['skip_provision', 'cleanup-reboot', 'rpm',
32 'modem_repair']
33
Xixuan Wue71c8932018-05-07 17:18:34 -070034
Aviv Keshet3baa47b2019-05-22 16:24:21 -070035def run_suite(test_specs, suite_handler, dry_run=False):
36 """Run a suite and wait for child results (if necessary)."""
Xixuan Wu56424bc2018-05-15 11:03:27 -070037 suite_id = os.environ.get('SWARMING_TASK_ID')
Aviv Keshet73b90662019-03-28 14:01:58 -070038 if not suite_id:
39 raise ValueError("Unable to determine suite's task id from env var "
40 "SWARMING_TASK_ID.")
Aviv Keshetd9935102019-03-18 14:44:28 -070041 _create_test_tasks(test_specs, suite_handler, suite_id, dry_run)
Xixuan Wua79b5f72018-12-26 12:29:39 -080042 suite_handler.set_suite_id(suite_id)
Xixuan Wuc7430712018-07-10 12:04:34 -070043
Aviv Keshet73b90662019-03-28 14:01:58 -070044 if suite_handler.should_wait():
Xixuan Wuc7430712018-07-10 12:04:34 -070045 _wait_for_results(suite_handler, dry_run=dry_run)
46
47
Aviv Keshetd9935102019-03-18 14:44:28 -070048def _create_test_tasks(test_specs, suite_handler, suite_id, dry_run=False):
49 """Create test tasks for a list of tests (TestSpecs).
Xixuan Wuc7430712018-07-10 12:04:34 -070050
Xixuan Wu5811e832018-07-12 11:56:24 -070051 Given a list of TestSpec object, this function will schedule them on
Xixuan Wuc7430712018-07-10 12:04:34 -070052 swarming one by one, and add them to the swarming_task_id-to-test map
53 of suite_handler to keep monitoring them.
54
Xixuan Wu5811e832018-07-12 11:56:24 -070055 @param test_specs: A list of cros_suite.TestSpec objects to schedule.
Xixuan Wuc7430712018-07-10 12:04:34 -070056 @param suite_handler: A cros_suite.SuiteHandler object to monitor the
57 test_specs' progress.
58 @param suite_id: A string ID for a suite task, it's the parent task id for
59 these to-be-scheduled test_specs.
60 @param dry_run: Whether to kick off dry runs of the tests.
61 """
Xixuan Wu5811e832018-07-12 11:56:24 -070062 for test_spec in test_specs:
Aviv Keshetd9935102019-03-18 14:44:28 -070063 test_task_id = _create_test_task(
Xixuan Wu5811e832018-07-12 11:56:24 -070064 test_spec,
Xixuan Wu5cb5a402018-06-04 16:37:23 -070065 suite_id=suite_id,
66 dry_run=dry_run)
Xixuan Wu9af95a22018-05-18 10:46:42 -070067 suite_handler.add_test_by_task_id(
68 test_task_id,
Xixuan Wu9d5d7032018-07-12 16:44:02 -070069 cros_suite.TestHandlerSpec(
Xixuan Wu5811e832018-07-12 11:56:24 -070070 test_spec=test_spec,
Alex Zamorzaev0cbec772019-04-12 16:26:54 -070071 remaining_retries=test_spec.test.job_retries,
Xixuan Wu9af95a22018-05-18 10:46:42 -070072 previous_retried_ids=[]))
Xixuan Wu56424bc2018-05-15 11:03:27 -070073
Xixuan Wu56424bc2018-05-15 11:03:27 -070074
Aviv Keshetea09ac32019-04-05 15:47:44 -070075def _create_test_task(test_spec, suite_id=None, dry_run=False):
Aviv Keshetd9935102019-03-18 14:44:28 -070076 """Create a test task for a given test spec.
Xixuan Wuc7bf77c2018-04-24 12:05:40 -070077
Xixuan Wu5811e832018-07-12 11:56:24 -070078 @param test_spec: A cros_suite.TestSpec object.
Xixuan Wu56424bc2018-05-15 11:03:27 -070079 @param suite_id: the suite task id of the test.
Aviv Keshetf0951212019-03-18 14:54:32 -070080 @param dry_run: If true, don't actually create task.
Xixuan Wu56424bc2018-05-15 11:03:27 -070081
82 @return the swarming task id of this task.
Xixuan Wuc7bf77c2018-04-24 12:05:40 -070083 """
Aviv Keshetd9935102019-03-18 14:44:28 -070084 logging.info('Creating task for test %s', test_spec.test.name)
Aviv Keshet2c25d062019-03-19 13:18:23 -070085 skylab_tool_path = os.environ.get('SKYLAB_TOOL', SKYLAB_TOOL)
Aviv Keshetf0951212019-03-18 14:54:32 -070086
87 cmd = [
Aviv Keshetaf016472019-08-01 13:36:36 -070088 skylab_tool_path, 'create-test', '-bb=False',
Aviv Keshetf0951212019-03-18 14:54:32 -070089 '-board', test_spec.board,
90 '-image', test_spec.build,
Aviv Keshet399692f2019-04-02 12:17:51 -070091 '-service-account-json', os.environ['SWARMING_CREDS'],
Xixuan Wu17dcb1d2019-04-19 14:33:25 -070092 '-priority', str(test_spec.priority),
Aviv Keshetf0f1b492019-04-03 15:30:48 -070093 '-timeout-mins', str(test_spec.execution_timeout_mins),
Aviv Keshetf0951212019-03-18 14:54:32 -070094 ]
Aviv Keshetb70c7092019-04-02 12:05:56 -070095 if _is_dev():
96 cmd += ['-dev']
Aviv Keshetf0951212019-03-18 14:54:32 -070097 if test_spec.pool:
98 # TODO(akeshet): Clean up this hack around pool name translation.
99 autotest_pool_label = 'pool:%s' % test_spec.pool
100 pool_dependency_value = swarming_lib.task_dependencies_from_labels(
101 [autotest_pool_label])['label-pool']
102 cmd += ['-pool', pool_dependency_value]
103
104 if test_spec.model:
105 cmd += ['-model', test_spec.model]
106 if test_spec.quota_account:
107 cmd += ['-qs-account', test_spec.quota_account]
108 if test_spec.test.test_type.lower() == 'client':
109 cmd += ['-client-test']
Aviv Keshet18e485f2019-05-02 16:04:20 -0700110 if suite_id is not None:
111 cmd += ['-parent-task-run-id', suite_id]
Aviv Keshetf0951212019-03-18 14:54:32 -0700112
Aviv Keshetf0f1b492019-04-03 15:30:48 -0700113
Aviv Keshetf0951212019-03-18 14:54:32 -0700114 tags = _compute_tags(test_spec.build, suite_id)
Aviv Keshet1df69632019-04-05 16:10:55 -0700115 dimensions = _compute_dimensions(test_spec.test.dependencies)
Aviv Keshetf0951212019-03-18 14:54:32 -0700116 keyvals_flat = _compute_job_keyvals_flat(test_spec.keyvals, suite_id)
117
Aviv Keshetd6321a12019-07-30 18:31:31 -0700118 for dim in dimensions:
119 cmd += ['-dim', dim]
Aviv Keshetf0951212019-03-18 14:54:32 -0700120 for tag in tags:
121 cmd += ['-tag', tag]
122 for keyval in keyvals_flat:
123 cmd += ['-keyval', keyval]
124 cmd += [test_spec.test.name]
Aviv Keshetf0951212019-03-18 14:54:32 -0700125
Xixuan Wue71c8932018-05-07 17:18:34 -0700126 if dry_run:
Aviv Keshetf0951212019-03-18 14:54:32 -0700127 logging.info('Would have created task with command %s', cmd)
128 return
Xixuan Wue71c8932018-05-07 17:18:34 -0700129
Aviv Keshetf0951212019-03-18 14:54:32 -0700130 # TODO(akeshet): Avoid this late chromite import.
131 cros_build_lib = autotest.chromite_load('cros_build_lib')
132 result = cros_build_lib.RunCommand(cmd, capture_output=True)
133 # TODO(akeshet): Use -json flag and json-parse output of the command instead
134 # of regex matching to determine task_id.
135 m = re.match('.*id=(.*)$', result.output)
136 task_id = m.group(1)
137 logging.info('Created task with id %s', task_id)
138 return task_id
Xixuan Wu3dea7cf2018-12-10 17:50:45 -0800139
Aviv Keshetf0951212019-03-18 14:54:32 -0700140
Aviv Keshetb70c7092019-04-02 12:05:56 -0700141# TODO(akeshet): Eliminate the need for this, by either adding an explicit
142# swarming_server argument to skylab tool, or having the tool respect the
143# SWARMING_SERVER environment variable. See crbug.com/948774
144def _is_dev():
145 """Detect whether skylab tool should be invoked with -dev flag."""
Aviv Keshetaa3839d2019-04-02 16:13:42 -0700146 return 'chromium-swarm-dev' in os.environ['SWARMING_SERVER']
Aviv Keshetb70c7092019-04-02 12:05:56 -0700147
Aviv Keshetf0951212019-03-18 14:54:32 -0700148def _compute_tags(build, suite_id):
149 tags = [
150 'build:%s' % build,
151 ]
152 if suite_id is not None:
153 tags += ['parent_task_id:%s' % suite_id]
154 return tags
155
156
Aviv Keshet1df69632019-04-05 16:10:55 -0700157def _compute_dimensions(dependencies):
Aviv Keshetf0951212019-03-18 14:54:32 -0700158 dimensions = []
Aviv Keshetf0951212019-03-18 14:54:32 -0700159 deps = _filter_unsupported_dependencies(dependencies)
160 flattened_swarming_deps = sorted([
161 '%s:%s' % (k, v) for
162 k, v in swarming_lib.task_dependencies_from_labels(deps).items()
163 ])
164 dimensions += flattened_swarming_deps
165 return dimensions
166
167
168def _compute_job_keyvals_flat(keyvals, suite_id):
169 # Job keyvals calculation.
170 job_keyvals = keyvals.copy()
171 if suite_id is not None:
172 # TODO(akeshet): Avoid this late autotest constants import.
173 constants = autotest.load('server.cros.dynamic_suite.constants')
174 job_keyvals[constants.PARENT_JOB_ID] = suite_id
175 keyvals_flat = sorted(
176 ['%s:%s' % (k, v) for k, v in job_keyvals.items()])
177 return keyvals_flat
178
179
180def _filter_unsupported_dependencies(dependencies):
181 """Filter out Skylab-unsupported test dependencies, with a warning."""
Xixuan Wudb053c82019-01-31 20:07:06 -0800182 deps = []
Aviv Keshetf0951212019-03-18 14:54:32 -0700183 for dep in dependencies:
Xixuan Wu79d14662018-08-20 11:15:41 -0700184 if dep in _NOT_SUPPORTED_DEPENDENCIES:
185 logging.warning('Dependency %s is not supported in skylab', dep)
Aviv Keshetf0951212019-03-18 14:54:32 -0700186 else:
187 deps.append(dep)
188 return deps
Xixuan Wu0bea9522018-05-08 17:49:19 -0700189
190
Xixuan Wu80795c82018-06-12 11:56:17 -0700191@contextlib.contextmanager
Xixuan Wuf52e40d2018-06-14 12:10:44 -0700192def disable_logging(logging_level):
193 """Context manager for disabling logging of a given logging level."""
Xixuan Wu80795c82018-06-12 11:56:17 -0700194 try:
Xixuan Wuf52e40d2018-06-14 12:10:44 -0700195 logging.disable(logging_level)
Xixuan Wu80795c82018-06-12 11:56:17 -0700196 yield
197 finally:
198 logging.disable(logging.NOTSET)
199
200
Xixuan Wuf52e40d2018-06-14 12:10:44 -0700201def _loop_and_wait_forever(suite_handler, dry_run):
202 """Wait for child tasks to finish or break."""
203 for iterations in itertools.count(0):
204 # Log progress every 300 seconds.
205 no_logging = bool(iterations * SUITE_WAIT_SLEEP_INTERVAL_SECONDS % 300)
206 with disable_logging(logging.INFO if no_logging else logging.NOTSET):
Xixuan Wuc6e28d32018-08-27 14:48:14 -0700207 suite_handler.handle_results(suite_handler.suite_id)
Xixuan Wuf52e40d2018-06-14 12:10:44 -0700208 if suite_handler.is_finished_waiting():
209 break
Xixuan Wu80795c82018-06-12 11:56:17 -0700210
Xixuan Wu4d5d0142018-08-27 15:26:58 -0700211 for t in suite_handler.retried_tasks:
212 _retry_test(suite_handler, t['task_id'], dry_run=dry_run)
213
Xixuan Wuf52e40d2018-06-14 12:10:44 -0700214 time.sleep(SUITE_WAIT_SLEEP_INTERVAL_SECONDS)
Xixuan Wu80795c82018-06-12 11:56:17 -0700215
216
Xixuan Wu9af95a22018-05-18 10:46:42 -0700217def _wait_for_results(suite_handler, dry_run=False):
Xixuan Wu2406be32018-05-14 13:51:30 -0700218 """Wait for child tasks to finish and return their results.
Xixuan Wu0bea9522018-05-08 17:49:19 -0700219
Xixuan Wu9af95a22018-05-18 10:46:42 -0700220 @param suite_handler: a cros_suite.SuiteHandler object.
Xixuan Wu2406be32018-05-14 13:51:30 -0700221 """
222 timeout_util = autotest.chromite_load('timeout_util')
Xixuan Wuf52e40d2018-06-14 12:10:44 -0700223 try:
Aviv Keshet5ae36712019-05-16 12:42:32 -0700224 with timeout_util.Timeout(suite_handler.timeout_mins * 60):
Xixuan Wuf52e40d2018-06-14 12:10:44 -0700225 _loop_and_wait_forever(suite_handler, dry_run)
226 except timeout_util.TimeoutError:
227 logging.error('Timeout in waiting for child tasks.')
228 return
Xixuan Wu0bea9522018-05-08 17:49:19 -0700229
230 logging.info('Finished to wait for child tasks.')
Xixuan Wu56424bc2018-05-15 11:03:27 -0700231
232
Xixuan Wu9af95a22018-05-18 10:46:42 -0700233def _retry_test(suite_handler, task_id, dry_run=False):
Xixuan Wu56424bc2018-05-15 11:03:27 -0700234 """Retry test for a suite.
235
236 We will execute the following actions for retrying a test:
237 1. Schedule the test.
238 2. Add the test with the new swarming task id to the suite's
239 retry handler, but reduce its remaining retries by 1.
240 3. Reduce the suite-level max retries by 1.
241 4. Remove prevous failed test from retry handler since it's not
242 actively monitored by the suite.
243
Xixuan Wu9af95a22018-05-18 10:46:42 -0700244 @param suite_handler: a cros_suite.SuiteHandler object.
Xixuan Wu56424bc2018-05-15 11:03:27 -0700245 @param task_id: The swarming task id for the retried test.
246 @param dry_run: Whether to retry a dry run of the test.
247 """
Xixuan Wu5811e832018-07-12 11:56:24 -0700248 last_retry_spec = suite_handler.get_test_by_task_id(task_id)
Xixuan Wu56424bc2018-05-15 11:03:27 -0700249 logging.info('Retrying test %s, remaining %d retries.',
Xixuan Wu5811e832018-07-12 11:56:24 -0700250 last_retry_spec.test_spec.test.name,
251 last_retry_spec.remaining_retries - 1)
Aviv Keshetd9935102019-03-18 14:44:28 -0700252 retried_task_id = _create_test_task(
Xixuan Wub60c1432019-03-07 17:15:39 +0000253 last_retry_spec.test_spec,
254 suite_id=suite_handler.suite_id,
Xixuan Wub60c1432019-03-07 17:15:39 +0000255 dry_run=dry_run)
Xixuan Wu5811e832018-07-12 11:56:24 -0700256 previous_retried_ids = last_retry_spec.previous_retried_ids + [task_id]
Xixuan Wu9af95a22018-05-18 10:46:42 -0700257 suite_handler.add_test_by_task_id(
258 retried_task_id,
Xixuan Wu9d5d7032018-07-12 16:44:02 -0700259 cros_suite.TestHandlerSpec(
Xixuan Wu5811e832018-07-12 11:56:24 -0700260 test_spec=last_retry_spec.test_spec,
261 remaining_retries=last_retry_spec.remaining_retries - 1,
Xixuan Wu9af95a22018-05-18 10:46:42 -0700262 previous_retried_ids=previous_retried_ids))
263 suite_handler.set_max_retries(suite_handler.max_retries - 1)
264 suite_handler.remove_test_by_task_id(task_id)
Xixuan Wu7a450c52018-07-20 15:07:51 -0700265
266
267def _convert_dict_to_string(input_dict):
268 """Convert dictionary to a string.
269
270 @param input_dict: A dictionary.
271 """
272 for k, v in input_dict.iteritems():
273 if isinstance(v, dict):
274 input_dict[k] = _convert_dict_to_string(v)
275 else:
276 input_dict[k] = str(v)
277
278 return json.dumps(input_dict)