blob: caa1f3d725c6994477efb21d749c0a70fe107d03 [file] [log] [blame]
Scott Zawalski20a9b582011-11-21 11:49:40 -08001#!/usr/bin/python
2#
3# Copyright (c) 2011 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
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
35
36# RegEx for extracting versions from build strings.
37_3_TUPLE_VERSION_RE = re.compile('R\d+-(\d+\.\d+\.\d+)')
38_4_TUPLE_VERSION_RE = re.compile('(\d+\.\d+\.\d+\.\d+)+-')
39
40
41def _ParseVersion(build):
42 """Extract version from build string. Parses x.x.x.x* and Ryy-x.x.x* forms."""
43 match = _3_TUPLE_VERSION_RE.match(build)
44 if not match:
45 match = _4_TUPLE_VERSION_RE.match(build)
46
47 # Will generate an exception if no match was found.
48 return match.group(1)
49
50
51class TestRunner(object):
52 """Helper class for scheduling jobs from tests and groups."""
53
54 def __init__(self, board, build, cli, config, dev, upload=False):
55 """Initializes class variables.
56
57 Args:
58 board: Board name for this build; e.g., x86-generic-rel
59 build: Full build string to look for; e.g., 0.8.61.0-r1cf43296-b269
60 cli: Path to Autotest CLI.
61 config: Dictionary of configuration as loaded from JSON.
62 dev: An initialized DevServer() instance.
63 upload: Whether to upload created job information to appengine.
64 """
65 self._board = board
66 self._build = build
67 self._config = config
68 self._cli = cli
69 self._dev = dev
70 self._upload = upload
71
72 def RunTest(self, job_name, platform, test, build=None, control_mods=None):
73 """Given a test dictionary: retrieves control file and creates jobs.
74
75 Test dictionary format is as follows:
76
77 {'name': '', 'control': '', 'count': ##, 'labels': [...], 'sync': <T/F>}
78
79 Optional keys are count, labels, and sync. If not specified they will be set
80 to default values of 1, None, and False respectively.
81
82 Jobs are created with the name <board>-<build>_<name>.
83
84 Args:
85 job_name: Name of job to create.
86 platform: Platform to schedule job for.
87 test: Test config dictionary.
88 build: Build to use, if different than the one used to initialize class.
89 control_mods: List of functions to call for control file preprocessing.
90 Each function will be passed the contents of the control file.
91
92 Raises:
93 common_util.ChromeOSTestError: If any steps fail.
94 """
95 # Initialize defaults for optional keys. Avoids tedious, if <key> in <test>
96 default = {'count': 1, 'labels': None, 'sync': None}
97 default.update(test)
98 test = default
99
100 if test['sync']:
101 test['sync'] = test['count']
102
103 if not build:
104 build = self._build
105
106 # Pull control file from Dev Server.
107 try:
108 control_file_data = self._dev.GetControlFile(
109 self._board, build, test['control'])
110 except common_util.ChromeOSTestError:
111 logging.error('Missing %s for %s on %s.', test['control'], job_name,
112 platform)
113 raise
114
115 # If there's any preprocessing to be done call it now.
116 if control_mods:
117 for mod in control_mods:
118 control_file_data = mod(control_file_data)
119
120 # Create temporary file and write control file contents to it.
121 temp_fd, temp_fn = tempfile.mkstemp()
122 os.write(temp_fd, control_file_data)
123 os.close(temp_fd)
124
125 # Create Autotest job using control file and image parameter.
126 try:
127 job_id = autotest_util.CreateJob(
128 name=job_name, control=temp_fn,
129 platforms='%d*%s' % (test['count'], platform), labels=test['labels'],
130 sync=test['sync'],
131 update_url=self._dev.GetUpdateUrl(self._board, build),
132 cli=self._cli)
133 finally:
134 # Cleanup temporary control file. Autotest doesn't need it anymore.
135 os.unlink(temp_fn)
136
137 #TODO(dalecurtis): Disabled, since it's not under active development.
138 #try:
139 # appengine_cfg = self._config.get('appengine', {})
140 # if self._upload and appengine_cfg:
141 # dash_util.UploadJob(appengine_cfg, job_id)
142 #except common_util.ChromeOSTestError:
143 # logging.warning('Failed to upload job to AppEngine.')
144
145 def RunTestGroups(self, groups, platform, lock=True):
146 """Given a list of test groups, creates Autotest jobs for associated tests.
147
148 Given a list of test groups, map each into the "groups" dictionary from the
149 JSON configuration file and launch associated tests. If lock is specified it
150 will attempt to acquire a dev server lock for each group before starting. If
151 a lock can't be obtained, the group won't be started.
152
153 Args:
154 groups: List of group names to run tests for. See test config for valid
155 group names.
156 platform: Platform label to look for. See test config for valid platforms.
157 lock: Attempt to acquire lock before running tests?
158 """
159 for group in groups:
160 if not group in self._config['groups']:
161 logging.warning('Skipping unknown group "%s".', group)
162 continue
163
164 # Start tests for the given group.
165 for test in self._config['groups'][group]:
166 has_lock = False
167 try:
168 job_name = '%s-%s_%s' % (self._board, self._build, test['name'])
169
170 # Attempt to acquire lock for test.
171 if lock:
172 tag = '%s/%s/%s_%s_%s' % (self._board, self._build, platform,
173 group, test['name'])
174 try:
175 self._dev.AcquireLock(tag)
176 has_lock = True
177 except common_util.ChromeOSTestError, e:
178 logging.debug('Refused lock for test "%s" from group "%s".'
179 ' Assuming it has already been started.',
180 test['name'], group)
181 continue
182
183 self.RunTest(platform=platform, test=test, job_name=job_name)
184 logging.info('Successfully created job "%s".', job_name)
185 except common_util.ChromeOSTestError, e:
186 logging.exception(e)
187 logging.error('Failed to schedule test "%s" from group "%s".',
188 test['name'], group)
189
190 # We failed, so release lock and let next run pick this test up.
191 if has_lock:
192 self._dev.ReleaseLock(tag)
193
194 def RunAutoupdateTests(self, platform):
195 # Process the autoupdate targets.
196 for target in self._dev.ListAutoupdateTargets(self._board, self._build):
197 has_lock = False
198 try:
199 # Tell other instances of the scheduler we're processing this target.
200 tag = '%s/%s/%s_%s' % (self._board, self._build, platform['platform'],
201 target)
202 try:
203 self._dev.AcquireLock(tag)
204 has_lock = True
205 except common_util.ChromeOSTestError, e:
206 logging.debug('Refused lock for autoupdate target "%s". Assuming'
207 ' it has already been started.', target)
208 continue
209
210 # Split target into base build and convenience label.
211 base_build, label = target.split('_')
212
213 # Setup preprocessing function to insert the correct update URL into
214 # the control file.
215 control_preprocess_fn = lambda x: x % {'update_url': '%s/%s/%s' % (
216 self._dev.GetUpdateUrl(
217 self._board, self._build), self._dev.AU_BASE, target)}
218
219 # E.g., x86-mario-r14-0.14.734.0_to_0.14.734.0-a1-b123_nton_au
220 job_name = '%s-%s_to_%s_%s_au' % (
221 self._board, _ParseVersion(base_build), self._build, label)
222
223 self.RunTest(
224 platform=platform['platform'],
225 test=self._config['groups']['autoupdate'][0], job_name=job_name,
226 build=base_build,
227 control_mods=[control_preprocess_fn])
228 logging.info('Successfully created job "%s".', job_name)
229 except common_util.ChromeOSTestError, e:
230 logging.exception(e)
231 logging.error('Failed to schedule autoupdate target "%s".', target)
232
233 # We failed, so release lock and let next run pick this target up.
234 if has_lock:
235 self._dev.ReleaseLock(tag)
236
237
238def ParseOptions():
239 """Parse command line options. Returns 2-tuple of options and config."""
240 parser = optparse.OptionParser('usage: %prog [options]')
241
242 # Add utility/helper class command line options.
243 test_config.AddOptions(parser)
244 log_util.AddOptions(parser)
245 autotest_util.AddOptions(parser, cli_only=True)
246
247 options = parser.parse_args()[0]
248 config = test_config.TestConfig(options.config)
249
250 return options, config.GetConfig()
251
252
253def main():
254 options, config = ParseOptions()
255
256 # Setup logger and enable verbose mode if specified.
257 log_util.InitializeLogging(options.verbose)
258
259 # Initialize Dev Server Utility class.
260 dev = dev_server.DevServer(**config['dev_server'])
261
262 # Main processing loop. Look for new builds of each board.
263 for board in config['boards']:
264 for platform in config['boards'][board]['platforms']:
265 logging.info('----[ Processing board %s, platform %s ]----',
266 board, platform['platform'])
267 try:
268 build = dev.GetLatestBuildVersion(board)
269 if not build:
270 continue
271
272 test_runner = TestRunner(
273 board=board, build=build, cli=options.cli, config=config, dev=dev,
274 upload=True)
275
276 # Determine which groups to run.
277 full_groups = []
278 if 'groups' in platform:
279 full_groups += platform['groups']
280 else:
281 full_groups += config['default_groups']
282
283 if 'extra_groups' in platform:
284 full_groups += platform['extra_groups']
285
286 test_runner.RunTestGroups(
287 groups=full_groups, platform=platform['platform'])
288
289 # Skip platforms which are not marked for AU testing.
290 if not platform.get('au_test', False):
291 continue
292
293 # Process AU targets.
294 test_runner.RunAutoupdateTests(platform)
295 except common_util.ChromeOSTestError, e:
296 logging.exception(e)
297 logging.warning('Exception encountered during processing. Skipping.')
298
299
300if __name__ == '__main__':
301 main()