Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 1 | #!/usr/bin/python |
| 2 | # |
Scott Zawalski | 372ec7c | 2012-03-07 18:11:34 -0500 | [diff] [blame] | 3 | # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 4 | # Use of this source code is governed by a BSD-style license that can be |
| 5 | # found in the LICENSE file. |
| 6 | |
| 7 | """Tool for scheduling BVT and full suite testing of Chrome OS images. |
| 8 | |
| 9 | Test Scheduler is a tool for scheduling the testing of Chrome OS images across |
| 10 | multiple boards and platforms. All testing is driven through a board to platform |
| 11 | mapping specified in a JSON config file. |
| 12 | |
| 13 | For each board, platform tuple the bvt group is scheduled. Once the bvt has |
| 14 | completed and passed, all groups from 'default_full_groups' are scheduled. |
| 15 | |
| 16 | Test Scheduler expects the JSON config file to be in the current working |
| 17 | directory or to be run with --config pointing to the actual config file. |
| 18 | """ |
| 19 | |
| 20 | __author__ = 'dalecurtis@google.com (Dale Curtis)' |
| 21 | |
| 22 | import logging |
| 23 | import optparse |
| 24 | import os |
| 25 | import re |
| 26 | import tempfile |
| 27 | |
| 28 | from chromeos_test import autotest_util |
| 29 | from chromeos_test import common_util |
| 30 | from chromeos_test import dash_util |
| 31 | from chromeos_test import dev_server |
| 32 | from chromeos_test import log_util |
| 33 | from chromeos_test import test_config |
| 34 | |
Scott Zawalski | 136dc4b | 2012-03-20 16:53:25 -0400 | [diff] [blame] | 35 | # Autotest imports |
| 36 | |
| 37 | import common |
| 38 | |
| 39 | from autotest_lib.client.common_lib.cros import dev_server as new_dev_server |
| 40 | |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 41 | |
| 42 | # RegEx for extracting versions from build strings. |
| 43 | _3_TUPLE_VERSION_RE = re.compile('R\d+-(\d+\.\d+\.\d+)') |
| 44 | _4_TUPLE_VERSION_RE = re.compile('(\d+\.\d+\.\d+\.\d+)+-') |
| 45 | |
| 46 | |
| 47 | def _ParseVersion(build): |
| 48 | """Extract version from build string. Parses x.x.x.x* and Ryy-x.x.x* forms.""" |
| 49 | match = _3_TUPLE_VERSION_RE.match(build) |
| 50 | if not match: |
| 51 | match = _4_TUPLE_VERSION_RE.match(build) |
| 52 | |
| 53 | # Will generate an exception if no match was found. |
| 54 | return match.group(1) |
| 55 | |
| 56 | |
| 57 | class TestRunner(object): |
| 58 | """Helper class for scheduling jobs from tests and groups.""" |
| 59 | |
Scott Zawalski | 9d7955f | 2012-03-20 20:30:51 -0400 | [diff] [blame] | 60 | def __init__(self, board, build, cli, config, dev, new_dev, upload=False): |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 61 | """Initializes class variables. |
| 62 | |
| 63 | Args: |
| 64 | board: Board name for this build; e.g., x86-generic-rel |
| 65 | build: Full build string to look for; e.g., 0.8.61.0-r1cf43296-b269 |
| 66 | cli: Path to Autotest CLI. |
| 67 | config: Dictionary of configuration as loaded from JSON. |
| 68 | dev: An initialized DevServer() instance. |
Scott Zawalski | 9d7955f | 2012-03-20 20:30:51 -0400 | [diff] [blame] | 69 | new_dev: new dev_server interface under client/common_lib/cros. |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 70 | upload: Whether to upload created job information to appengine. |
| 71 | """ |
| 72 | self._board = board |
| 73 | self._build = build |
| 74 | self._config = config |
| 75 | self._cli = cli |
| 76 | self._dev = dev |
Scott Zawalski | 9d7955f | 2012-03-20 20:30:51 -0400 | [diff] [blame] | 77 | self._new_dev = new_dev |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 78 | self._upload = upload |
| 79 | |
| 80 | def RunTest(self, job_name, platform, test, build=None, control_mods=None): |
| 81 | """Given a test dictionary: retrieves control file and creates jobs. |
| 82 | |
| 83 | Test dictionary format is as follows: |
| 84 | |
| 85 | {'name': '', 'control': '', 'count': ##, 'labels': [...], 'sync': <T/F>} |
| 86 | |
| 87 | Optional keys are count, labels, and sync. If not specified they will be set |
| 88 | to default values of 1, None, and False respectively. |
| 89 | |
| 90 | Jobs are created with the name <board>-<build>_<name>. |
| 91 | |
| 92 | Args: |
| 93 | job_name: Name of job to create. |
| 94 | platform: Platform to schedule job for. |
| 95 | test: Test config dictionary. |
| 96 | build: Build to use, if different than the one used to initialize class. |
| 97 | control_mods: List of functions to call for control file preprocessing. |
| 98 | Each function will be passed the contents of the control file. |
| 99 | |
| 100 | Raises: |
| 101 | common_util.ChromeOSTestError: If any steps fail. |
| 102 | """ |
| 103 | # Initialize defaults for optional keys. Avoids tedious, if <key> in <test> |
| 104 | default = {'count': 1, 'labels': None, 'sync': None} |
| 105 | default.update(test) |
| 106 | test = default |
| 107 | |
| 108 | if test['sync']: |
| 109 | test['sync'] = test['count'] |
| 110 | |
| 111 | if not build: |
| 112 | build = self._build |
| 113 | |
| 114 | # Pull control file from Dev Server. |
| 115 | try: |
Scott Zawalski | 9d7955f | 2012-03-20 20:30:51 -0400 | [diff] [blame] | 116 | # Use new style for TOT boards. |
| 117 | if 'release' in self._board: |
| 118 | image = '%s/%s' % (self._board, build) |
| 119 | # Make sure the latest board is already staged. This will hang until |
| 120 | # the image is properly staged or return immediately if it is already |
| 121 | # staged. This will have little impact on the rest of this process and |
| 122 | # ensures we properly launch tests while straddling the old and the new |
| 123 | # styles. |
| 124 | self._new_dev.trigger_download(image) |
| 125 | control_file_data = self._new_dev.get_control_file(image, |
| 126 | test['control']) |
Scott Zawalski | de734bd | 2012-03-23 15:57:06 -0400 | [diff] [blame] | 127 | if 'Unknown control path' in control_file_data: |
| 128 | raise common_util.ChromeOSTestError( |
| 129 | 'Control file %s not yet staged, skipping' % test['control']) |
Scott Zawalski | 9d7955f | 2012-03-20 20:30:51 -0400 | [diff] [blame] | 130 | else: |
| 131 | control_file_data = self._dev.GetControlFile(self._board, build, |
| 132 | test['control']) |
Scott Zawalski | 1799364 | 2012-05-17 13:45:16 -0400 | [diff] [blame] | 133 | except (new_dev_server.DevServerException, common_util.ChromeOSTestError): |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 134 | logging.error('Missing %s for %s on %s.', test['control'], job_name, |
| 135 | platform) |
| 136 | raise |
| 137 | |
| 138 | # If there's any preprocessing to be done call it now. |
| 139 | if control_mods: |
| 140 | for mod in control_mods: |
| 141 | control_file_data = mod(control_file_data) |
| 142 | |
| 143 | # Create temporary file and write control file contents to it. |
| 144 | temp_fd, temp_fn = tempfile.mkstemp() |
| 145 | os.write(temp_fd, control_file_data) |
| 146 | os.close(temp_fd) |
| 147 | |
| 148 | # Create Autotest job using control file and image parameter. |
| 149 | try: |
Scott Zawalski | 1089941 | 2012-03-08 17:17:26 -0500 | [diff] [blame] | 150 | # Inflate the priority of BVT runs. |
| 151 | if job_name.endswith('_bvt'): |
| 152 | priority = 'urgent' |
| 153 | else: |
| 154 | priority = 'medium' |
| 155 | |
Scott Zawalski | 10aebfc | 2012-07-10 19:16:00 -0400 | [diff] [blame] | 156 | # Add pool:suites to all jobs to avoid using the BVT machines with the |
| 157 | # same platform label. |
| 158 | if test['labels'] is None: |
| 159 | test['labels'] = ['pool:suites'] |
| 160 | else: |
| 161 | test['labels'].append('pool:suites') |
| 162 | |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 163 | job_id = autotest_util.CreateJob( |
| 164 | name=job_name, control=temp_fn, |
| 165 | platforms='%d*%s' % (test['count'], platform), labels=test['labels'], |
| 166 | sync=test['sync'], |
| 167 | update_url=self._dev.GetUpdateUrl(self._board, build), |
Scott Zawalski | 1089941 | 2012-03-08 17:17:26 -0500 | [diff] [blame] | 168 | cli=self._cli, priority=priority) |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 169 | finally: |
| 170 | # Cleanup temporary control file. Autotest doesn't need it anymore. |
| 171 | os.unlink(temp_fn) |
| 172 | |
| 173 | #TODO(dalecurtis): Disabled, since it's not under active development. |
| 174 | #try: |
| 175 | # appengine_cfg = self._config.get('appengine', {}) |
| 176 | # if self._upload and appengine_cfg: |
| 177 | # dash_util.UploadJob(appengine_cfg, job_id) |
| 178 | #except common_util.ChromeOSTestError: |
| 179 | # logging.warning('Failed to upload job to AppEngine.') |
| 180 | |
| 181 | def RunTestGroups(self, groups, platform, lock=True): |
| 182 | """Given a list of test groups, creates Autotest jobs for associated tests. |
| 183 | |
| 184 | Given a list of test groups, map each into the "groups" dictionary from the |
| 185 | JSON configuration file and launch associated tests. If lock is specified it |
| 186 | will attempt to acquire a dev server lock for each group before starting. If |
| 187 | a lock can't be obtained, the group won't be started. |
| 188 | |
| 189 | Args: |
| 190 | groups: List of group names to run tests for. See test config for valid |
| 191 | group names. |
| 192 | platform: Platform label to look for. See test config for valid platforms. |
| 193 | lock: Attempt to acquire lock before running tests? |
| 194 | """ |
| 195 | for group in groups: |
| 196 | if not group in self._config['groups']: |
| 197 | logging.warning('Skipping unknown group "%s".', group) |
| 198 | continue |
| 199 | |
| 200 | # Start tests for the given group. |
| 201 | for test in self._config['groups'][group]: |
| 202 | has_lock = False |
| 203 | try: |
| 204 | job_name = '%s-%s_%s' % (self._board, self._build, test['name']) |
| 205 | |
| 206 | # Attempt to acquire lock for test. |
| 207 | if lock: |
| 208 | tag = '%s/%s/%s_%s_%s' % (self._board, self._build, platform, |
| 209 | group, test['name']) |
| 210 | try: |
| 211 | self._dev.AcquireLock(tag) |
| 212 | has_lock = True |
| 213 | except common_util.ChromeOSTestError, e: |
| 214 | logging.debug('Refused lock for test "%s" from group "%s".' |
| 215 | ' Assuming it has already been started.', |
| 216 | test['name'], group) |
| 217 | continue |
| 218 | |
| 219 | self.RunTest(platform=platform, test=test, job_name=job_name) |
| 220 | logging.info('Successfully created job "%s".', job_name) |
| 221 | except common_util.ChromeOSTestError, e: |
| 222 | logging.exception(e) |
| 223 | logging.error('Failed to schedule test "%s" from group "%s".', |
| 224 | test['name'], group) |
| 225 | |
| 226 | # We failed, so release lock and let next run pick this test up. |
| 227 | if has_lock: |
| 228 | self._dev.ReleaseLock(tag) |
| 229 | |
| 230 | def RunAutoupdateTests(self, platform): |
| 231 | # Process the autoupdate targets. |
| 232 | for target in self._dev.ListAutoupdateTargets(self._board, self._build): |
| 233 | has_lock = False |
| 234 | try: |
| 235 | # Tell other instances of the scheduler we're processing this target. |
| 236 | tag = '%s/%s/%s_%s' % (self._board, self._build, platform['platform'], |
| 237 | target) |
| 238 | try: |
| 239 | self._dev.AcquireLock(tag) |
| 240 | has_lock = True |
| 241 | except common_util.ChromeOSTestError, e: |
| 242 | logging.debug('Refused lock for autoupdate target "%s". Assuming' |
| 243 | ' it has already been started.', target) |
| 244 | continue |
| 245 | |
| 246 | # Split target into base build and convenience label. |
| 247 | base_build, label = target.split('_') |
| 248 | |
| 249 | # Setup preprocessing function to insert the correct update URL into |
| 250 | # the control file. |
| 251 | control_preprocess_fn = lambda x: x % {'update_url': '%s/%s/%s' % ( |
| 252 | self._dev.GetUpdateUrl( |
| 253 | self._board, self._build), self._dev.AU_BASE, target)} |
| 254 | |
| 255 | # E.g., x86-mario-r14-0.14.734.0_to_0.14.734.0-a1-b123_nton_au |
| 256 | job_name = '%s-%s_to_%s_%s_au' % ( |
| 257 | self._board, _ParseVersion(base_build), self._build, label) |
| 258 | |
| 259 | self.RunTest( |
| 260 | platform=platform['platform'], |
| 261 | test=self._config['groups']['autoupdate'][0], job_name=job_name, |
| 262 | build=base_build, |
| 263 | control_mods=[control_preprocess_fn]) |
| 264 | logging.info('Successfully created job "%s".', job_name) |
| 265 | except common_util.ChromeOSTestError, e: |
| 266 | logging.exception(e) |
| 267 | logging.error('Failed to schedule autoupdate target "%s".', target) |
| 268 | |
| 269 | # We failed, so release lock and let next run pick this target up. |
| 270 | if has_lock: |
| 271 | self._dev.ReleaseLock(tag) |
| 272 | |
| 273 | |
| 274 | def ParseOptions(): |
| 275 | """Parse command line options. Returns 2-tuple of options and config.""" |
| 276 | parser = optparse.OptionParser('usage: %prog [options]') |
| 277 | |
| 278 | # Add utility/helper class command line options. |
| 279 | test_config.AddOptions(parser) |
| 280 | log_util.AddOptions(parser) |
| 281 | autotest_util.AddOptions(parser, cli_only=True) |
| 282 | |
| 283 | options = parser.parse_args()[0] |
| 284 | config = test_config.TestConfig(options.config) |
| 285 | |
| 286 | return options, config.GetConfig() |
| 287 | |
| 288 | |
| 289 | def main(): |
| 290 | options, config = ParseOptions() |
| 291 | |
| 292 | # Setup logger and enable verbose mode if specified. |
| 293 | log_util.InitializeLogging(options.verbose) |
| 294 | |
| 295 | # Initialize Dev Server Utility class. |
| 296 | dev = dev_server.DevServer(**config['dev_server']) |
| 297 | |
| 298 | # Main processing loop. Look for new builds of each board. |
| 299 | for board in config['boards']: |
| 300 | for platform in config['boards'][board]['platforms']: |
| 301 | logging.info('----[ Processing board %s, platform %s ]----', |
| 302 | board, platform['platform']) |
| 303 | try: |
Scott Zawalski | 136dc4b | 2012-03-20 16:53:25 -0400 | [diff] [blame] | 304 | new_dev = new_dev_server.DevServer() |
| 305 | # The variable board is akin to target in the new nomenclature. This is |
| 306 | # the old style and the new style clashing. |
Scott Zawalski | 4ec0cd6 | 2012-05-12 16:48:25 -0400 | [diff] [blame] | 307 | # TODO(scottz): remove kludge once we move to suite scheduler. |
Scott Zawalski | ec41205 | 2012-06-15 15:01:26 -0400 | [diff] [blame] | 308 | for milestone in ['r19', 'r20']: |
Scott Zawalski | 40a3686 | 2012-05-30 17:49:45 -0400 | [diff] [blame] | 309 | try: |
| 310 | build = new_dev.get_latest_build(board, milestone=milestone) |
| 311 | except new_dev_server.DevServerException: |
| 312 | continue |
| 313 | # Leave just in case we do get an empty response from the server |
| 314 | # but we shouldn't. |
Scott Zawalski | 63d4507 | 2012-04-06 16:01:07 -0400 | [diff] [blame] | 315 | if not build: |
| 316 | continue |
| 317 | test_runner = TestRunner( |
| 318 | board=board, build=build, cli=options.cli, config=config, |
| 319 | dev=dev, new_dev=new_dev, upload=True) |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 320 | |
Scott Zawalski | 63d4507 | 2012-04-06 16:01:07 -0400 | [diff] [blame] | 321 | # Determine which groups to run. |
| 322 | full_groups = [] |
| 323 | if 'groups' in platform: |
| 324 | full_groups += platform['groups'] |
Scott Zawalski | 372ec7c | 2012-03-07 18:11:34 -0500 | [diff] [blame] | 325 | else: |
Scott Zawalski | 63d4507 | 2012-04-06 16:01:07 -0400 | [diff] [blame] | 326 | # Add default groups to the job since 'groups' was not defined. |
| 327 | # if test_suite is set to True use 'default_tot_groups' from the |
| 328 | # json configuration, otherwise use 'default_groups.' |
| 329 | if platform.get('test_suite'): |
| 330 | full_groups += config['default_tot_groups'] |
| 331 | else: |
| 332 | full_groups += config['default_groups'] |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 333 | |
Scott Zawalski | 63d4507 | 2012-04-06 16:01:07 -0400 | [diff] [blame] | 334 | if 'extra_groups' in platform: |
| 335 | full_groups += platform['extra_groups'] |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 336 | |
Scott Zawalski | 63d4507 | 2012-04-06 16:01:07 -0400 | [diff] [blame] | 337 | test_runner.RunTestGroups( |
| 338 | groups=full_groups, platform=platform['platform']) |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 339 | |
Scott Zawalski | 63d4507 | 2012-04-06 16:01:07 -0400 | [diff] [blame] | 340 | # Skip platforms which are not marked for AU testing. |
| 341 | if not platform.get('au_test', False): |
| 342 | continue |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 343 | |
Scott Zawalski | 63d4507 | 2012-04-06 16:01:07 -0400 | [diff] [blame] | 344 | # Process AU targets. |
| 345 | test_runner.RunAutoupdateTests(platform) |
Scott Zawalski | 1799364 | 2012-05-17 13:45:16 -0400 | [diff] [blame] | 346 | except (new_dev_server.DevServerException, |
| 347 | common_util.ChromeOSTestError) as e: |
Scott Zawalski | 20a9b58 | 2011-11-21 11:49:40 -0800 | [diff] [blame] | 348 | logging.exception(e) |
| 349 | logging.warning('Exception encountered during processing. Skipping.') |
| 350 | |
| 351 | |
| 352 | if __name__ == '__main__': |
| 353 | main() |