blob: abda3ff18fc25dcf679f0c46d6bc6484b0f2973a [file] [log] [blame]
Scott Zawalski20a9b582011-11-21 11:49:40 -08001#!/usr/bin/python
2#
Scott Zawalski372ec7c2012-03-07 18:11:34 -05003# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Scott Zawalski20a9b582011-11-21 11:49:40 -08004# 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
9Test Scheduler is a tool for scheduling the testing of Chrome OS images across
10multiple boards and platforms. All testing is driven through a board to platform
11mapping specified in a JSON config file.
12
13For each board, platform tuple the bvt group is scheduled. Once the bvt has
14completed and passed, all groups from 'default_full_groups' are scheduled.
15
16Test Scheduler expects the JSON config file to be in the current working
17directory or to be run with --config pointing to the actual config file.
18"""
19
20__author__ = 'dalecurtis@google.com (Dale Curtis)'
21
22import logging
23import optparse
24import os
25import re
26import tempfile
27
28from chromeos_test import autotest_util
29from chromeos_test import common_util
30from chromeos_test import dash_util
31from chromeos_test import dev_server
32from chromeos_test import log_util
33from chromeos_test import test_config
34
Scott Zawalski136dc4b2012-03-20 16:53:25 -040035# Autotest imports
36
37import common
38
39from autotest_lib.client.common_lib.cros import dev_server as new_dev_server
40
Scott Zawalski20a9b582011-11-21 11:49:40 -080041
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
47def _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
57class TestRunner(object):
58 """Helper class for scheduling jobs from tests and groups."""
59
Scott Zawalski9d7955f2012-03-20 20:30:51 -040060 def __init__(self, board, build, cli, config, dev, new_dev, upload=False):
Scott Zawalski20a9b582011-11-21 11:49:40 -080061 """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 Zawalski9d7955f2012-03-20 20:30:51 -040069 new_dev: new dev_server interface under client/common_lib/cros.
Scott Zawalski20a9b582011-11-21 11:49:40 -080070 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 Zawalski9d7955f2012-03-20 20:30:51 -040077 self._new_dev = new_dev
Scott Zawalski20a9b582011-11-21 11:49:40 -080078 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 Zawalski9d7955f2012-03-20 20:30:51 -0400116 # 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 Zawalskide734bd2012-03-23 15:57:06 -0400127 if 'Unknown control path' in control_file_data:
128 raise common_util.ChromeOSTestError(
129 'Control file %s not yet staged, skipping' % test['control'])
Scott Zawalski9d7955f2012-03-20 20:30:51 -0400130 else:
131 control_file_data = self._dev.GetControlFile(self._board, build,
132 test['control'])
Scott Zawalski17993642012-05-17 13:45:16 -0400133 except (new_dev_server.DevServerException, common_util.ChromeOSTestError):
Scott Zawalski20a9b582011-11-21 11:49:40 -0800134 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 Zawalski10899412012-03-08 17:17:26 -0500150 # Inflate the priority of BVT runs.
151 if job_name.endswith('_bvt'):
152 priority = 'urgent'
153 else:
154 priority = 'medium'
155
Scott Zawalski10aebfc2012-07-10 19:16:00 -0400156 # 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 Zawalski20a9b582011-11-21 11:49:40 -0800163 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 Zawalski10899412012-03-08 17:17:26 -0500168 cli=self._cli, priority=priority)
Scott Zawalski20a9b582011-11-21 11:49:40 -0800169 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
274def 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
289def 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 Zawalski136dc4b2012-03-20 16:53:25 -0400304 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 Zawalski4ec0cd62012-05-12 16:48:25 -0400307 # TODO(scottz): remove kludge once we move to suite scheduler.
Scott Zawalskiec412052012-06-15 15:01:26 -0400308 for milestone in ['r19', 'r20']:
Scott Zawalski40a36862012-05-30 17:49:45 -0400309 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 Zawalski63d45072012-04-06 16:01:07 -0400315 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 Zawalski20a9b582011-11-21 11:49:40 -0800320
Scott Zawalski63d45072012-04-06 16:01:07 -0400321 # Determine which groups to run.
322 full_groups = []
323 if 'groups' in platform:
324 full_groups += platform['groups']
Scott Zawalski372ec7c2012-03-07 18:11:34 -0500325 else:
Scott Zawalski63d45072012-04-06 16:01:07 -0400326 # 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 Zawalski20a9b582011-11-21 11:49:40 -0800333
Scott Zawalski63d45072012-04-06 16:01:07 -0400334 if 'extra_groups' in platform:
335 full_groups += platform['extra_groups']
Scott Zawalski20a9b582011-11-21 11:49:40 -0800336
Scott Zawalski63d45072012-04-06 16:01:07 -0400337 test_runner.RunTestGroups(
338 groups=full_groups, platform=platform['platform'])
Scott Zawalski20a9b582011-11-21 11:49:40 -0800339
Scott Zawalski63d45072012-04-06 16:01:07 -0400340 # Skip platforms which are not marked for AU testing.
341 if not platform.get('au_test', False):
342 continue
Scott Zawalski20a9b582011-11-21 11:49:40 -0800343
Scott Zawalski63d45072012-04-06 16:01:07 -0400344 # Process AU targets.
345 test_runner.RunAutoupdateTests(platform)
Scott Zawalski17993642012-05-17 13:45:16 -0400346 except (new_dev_server.DevServerException,
347 common_util.ChromeOSTestError) as e:
Scott Zawalski20a9b582011-11-21 11:49:40 -0800348 logging.exception(e)
349 logging.warning('Exception encountered during processing. Skipping.')
350
351
352if __name__ == '__main__':
353 main()