blob: f22d031cf1e3a9284ca779646dc218d4bbb26578 [file] [log] [blame]
Aviv Keshet6f455262013-03-01 16:02:29 -08001# pylint: disable-msg=C0111
mbligh99c2c6f2008-07-11 18:15:46 +00002# Copyright 2008 Google Inc. Released under the GPL v2
3
J. Richard Barnette89d5d212013-05-21 13:14:48 -07004import warnings
5with warnings.catch_warnings():
6 # The 'compiler' module is gone in Python 3.0. Let's not say
7 # so in every log file.
8 warnings.simplefilter("ignore", DeprecationWarning)
9 import compiler
10import logging, textwrap
mbligh99c2c6f2008-07-11 18:15:46 +000011
Dan Shief5b53f2013-01-22 10:22:01 -080012from autotest_lib.client.common_lib import enum
mbligh99c2c6f2008-07-11 18:15:46 +000013
Scott Zawalski8ad52b62012-10-29 09:59:33 -040014REQUIRED_VARS = set(['author', 'doc', 'name', 'time', 'test_type'])
J. Richard Barnette42384782014-05-09 10:47:24 -070015OBSOLETE_VARS = set(['experimental'])
mbligh99c2c6f2008-07-11 18:15:46 +000016
Aviv Keshet3dd8beb2013-05-13 17:36:04 -070017CONTROL_TYPE = enum.Enum('Server', 'Client', start_value=1)
18CONTROL_TYPE_NAMES = enum.Enum(*CONTROL_TYPE.names, string_values=True)
19
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -080020_SUITE_ATTRIBUTE_PREFIX = 'suite:'
21
mbligh99c2c6f2008-07-11 18:15:46 +000022class ControlVariableException(Exception):
23 pass
24
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -080025def _validate_control_file_fields(control_file_path, control_file_vars,
26 raise_warnings):
27 """Validate the given set of variables from a control file.
28
29 @param control_file_path: string path of the control file these were
30 loaded from.
31 @param control_file_vars: dict of variables set in a control file.
32 @param raise_warnings: True iff we should raise on invalid variables.
33
34 """
35 diff = REQUIRED_VARS - set(control_file_vars)
36 if diff:
37 warning = ('WARNING: Not all required control '
38 'variables were specified in %s. Please define '
39 '%s.') % (control_file_path, ', '.join(diff))
40 if raise_warnings:
41 raise ControlVariableException(warning)
42 print textwrap.wrap(warning, 80)
43
44 obsolete = OBSOLETE_VARS & set(control_file_vars)
45 if obsolete:
46 warning = ('WARNING: Obsolete variables were '
47 'specified in %s. Please remove '
48 '%s.') % (control_file_path, ', '.join(obsolete))
49 if raise_warnings:
50 raise ControlVariableException(warning)
51 print textwrap.wrap(warning, 80)
52
mbligh99c2c6f2008-07-11 18:15:46 +000053
54class ControlData(object):
Dan Shief5b53f2013-01-22 10:22:01 -080055 # Available TIME settings in control file, the list must be in lower case
56 # and in ascending order, test running faster comes first.
57 TEST_TIME_LIST = ['fast', 'short', 'medium', 'long', 'lengthy']
58 TEST_TIME = enum.Enum(*TEST_TIME_LIST, string_values=False)
59
Dan Shief5b53f2013-01-22 10:22:01 -080060 @staticmethod
61 def get_test_time_index(time):
62 """
63 Get the order of estimated test time, based on the TIME setting in
64 Control file. Faster test gets a lower index number.
65 """
66 try:
67 return ControlData.TEST_TIME.get_value(time.lower())
68 except AttributeError:
69 # Raise exception if time value is not a valid TIME setting.
70 error_msg = '%s is not a valid TIME.' % time
71 logging.error(error_msg)
72 raise ControlVariableException(error_msg)
73
74
mblighd7cd9832008-10-02 16:20:37 +000075 def __init__(self, vars, path, raise_warnings=False):
mbligh99c2c6f2008-07-11 18:15:46 +000076 # Defaults
mblighd7cd9832008-10-02 16:20:37 +000077 self.path = path
mbligh99c2c6f2008-07-11 18:15:46 +000078 self.dependencies = set()
J. Richard Barnette42384782014-05-09 10:47:24 -070079 # TODO(jrbarnette): This should be removed once outside
80 # code that uses can be changed.
mbligh99c2c6f2008-07-11 18:15:46 +000081 self.experimental = False
82 self.run_verify = True
83 self.sync_count = 1
Eric Li861b2d52011-02-04 14:50:35 -080084 self.test_parameters = set()
Scott Zawalski8ad52b62012-10-29 09:59:33 -040085 self.test_category = ''
86 self.test_class = ''
Aviv Keshet6f455262013-03-01 16:02:29 -080087 self.retries = 0
Fang Denge3bc24b2014-03-17 15:19:46 -070088 self.job_retries = 0
Dan Shiec1d47d2015-02-13 11:38:13 -080089 # Default to require server-side package. Unless require_ssp is
90 # explicitly set to False, server-side package will be used for the
91 # job. This can be overridden by global config
92 # AUTOSERV/enable_ssp_container
93 self.require_ssp = None
Shuqian Zhao13248c52015-03-27 13:32:17 -070094 self.attributes = set()
mbligh99c2c6f2008-07-11 18:15:46 +000095
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -080096 _validate_control_file_fields(self.path, vars, raise_warnings)
mbligh99c2c6f2008-07-11 18:15:46 +000097
98 for key, val in vars.iteritems():
99 try:
100 self.set_attr(key, val, raise_warnings)
101 except Exception, e:
102 if raise_warnings:
103 raise
J. Richard Barnette42384782014-05-09 10:47:24 -0700104 print 'WARNING: %s; skipping' % e
mbligh99c2c6f2008-07-11 18:15:46 +0000105
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -0800106 self._patch_up_suites_from_attributes()
107
mbligh99c2c6f2008-07-11 18:15:46 +0000108
Allen Lif20e17d2017-01-03 18:24:19 -0800109 @property
110 def suite_tag_parts(self):
111 """Return the part strings of the test's suite tag."""
112 if hasattr(self, 'suite'):
113 return [part.strip() for part in self.suite.split(',')]
114 else:
115 return []
116
117
mbligh99c2c6f2008-07-11 18:15:46 +0000118 def set_attr(self, attr, val, raise_warnings=False):
119 attr = attr.lower()
120 try:
121 set_fn = getattr(self, 'set_%s' % attr)
122 set_fn(val)
123 except AttributeError:
124 # This must not be a variable we care about
125 pass
126
127
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -0800128 def _patch_up_suites_from_attributes(self):
129 """Patch up the set of suites this test is part of.
130
Christopher Wileybfec5922016-03-07 09:11:32 -0800131 Legacy builds will not have an appropriate ATTRIBUTES field set.
132 Take the union of suites specified via ATTRIBUTES and suites specified
133 via SUITE.
134
135 SUITE used to be its own variable, but now suites are taken only from
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -0800136 the attributes.
137
138 """
Christopher Wileybfec5922016-03-07 09:11:32 -0800139
140 suite_names = set()
141 # Extract any suites we know ourselves to be in based on the SUITE
142 # line. This line is deprecated, but control files in old builds will
143 # still have it.
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -0800144 if hasattr(self, 'suite'):
Christopher Wileybfec5922016-03-07 09:11:32 -0800145 existing_suites = self.suite.split(',')
146 existing_suites = [name.strip() for name in existing_suites]
147 existing_suites = [name for name in existing_suites if name]
148 suite_names.update(existing_suites)
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -0800149
150 # Figure out if our attributes mention any suites.
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -0800151 for attribute in self.attributes:
152 if not attribute.startswith(_SUITE_ATTRIBUTE_PREFIX):
153 continue
154 suite_name = attribute[len(_SUITE_ATTRIBUTE_PREFIX):]
Christopher Wileybfec5922016-03-07 09:11:32 -0800155 suite_names.add(suite_name)
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -0800156
157 # Rebuild the suite field if necessary.
158 if suite_names:
Christopher Wiley4a140562016-03-08 09:36:24 -0800159 self.set_suite(','.join(sorted(list(suite_names))))
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -0800160
161
mbligh99c2c6f2008-07-11 18:15:46 +0000162 def _set_string(self, attr, val):
163 val = str(val)
164 setattr(self, attr, val)
165
166
167 def _set_option(self, attr, val, options):
168 val = str(val)
169 if val.lower() not in [x.lower() for x in options]:
170 raise ValueError("%s must be one of the following "
171 "options: %s" % (attr,
172 ', '.join(options)))
173 setattr(self, attr, val)
174
175
176 def _set_bool(self, attr, val):
177 val = str(val).lower()
178 if val == "false":
179 val = False
180 elif val == "true":
181 val = True
182 else:
183 msg = "%s must be either true or false" % attr
184 raise ValueError(msg)
185 setattr(self, attr, val)
186
187
188 def _set_int(self, attr, val, min=None, max=None):
189 val = int(val)
190 if min is not None and min > val:
191 raise ValueError("%s is %d, which is below the "
192 "minimum of %d" % (attr, val, min))
193 if max is not None and max < val:
194 raise ValueError("%s is %d, which is above the "
195 "maximum of %d" % (attr, val, max))
196 setattr(self, attr, val)
197
198
199 def _set_set(self, attr, val):
200 val = str(val)
Dan Shicddad392016-03-23 15:15:07 -0700201 items = [x.strip() for x in val.split(',') if x.strip()]
mbligh99c2c6f2008-07-11 18:15:46 +0000202 setattr(self, attr, set(items))
203
204
205 def set_author(self, val):
206 self._set_string('author', val)
207
208
209 def set_dependencies(self, val):
210 self._set_set('dependencies', val)
211
212
213 def set_doc(self, val):
214 self._set_string('doc', val)
215
216
mbligh99c2c6f2008-07-11 18:15:46 +0000217 def set_name(self, val):
218 self._set_string('name', val)
219
220
221 def set_run_verify(self, val):
222 self._set_bool('run_verify', val)
223
224
225 def set_sync_count(self, val):
226 self._set_int('sync_count', val, min=1)
227
228
Chris Masone6fed6462011-10-20 16:36:43 -0700229 def set_suite(self, val):
230 self._set_string('suite', val)
231
232
mbligh99c2c6f2008-07-11 18:15:46 +0000233 def set_time(self, val):
Dan Shief5b53f2013-01-22 10:22:01 -0800234 self._set_option('time', val, ControlData.TEST_TIME_LIST)
mbligh99c2c6f2008-07-11 18:15:46 +0000235
236
237 def set_test_class(self, val):
238 self._set_string('test_class', val.lower())
239
240
241 def set_test_category(self, val):
242 self._set_string('test_category', val.lower())
243
244
245 def set_test_type(self, val):
Aviv Keshet3dd8beb2013-05-13 17:36:04 -0700246 self._set_option('test_type', val, list(CONTROL_TYPE.names))
mbligh99c2c6f2008-07-11 18:15:46 +0000247
Eric Li861b2d52011-02-04 14:50:35 -0800248
Eric Lid3e8a3b2010-12-23 10:02:07 -0800249 def set_test_parameters(self, val):
250 self._set_set('test_parameters', val)
251
mbligh99c2c6f2008-07-11 18:15:46 +0000252
Aviv Keshet6f455262013-03-01 16:02:29 -0800253 def set_retries(self, val):
254 self._set_int('retries', val)
255
Dan Shiec1d47d2015-02-13 11:38:13 -0800256
Fang Denge3bc24b2014-03-17 15:19:46 -0700257 def set_job_retries(self, val):
258 self._set_int('job_retries', val)
259
Aviv Keshet6f455262013-03-01 16:02:29 -0800260
Prashanth Bee707312014-03-31 13:00:36 -0700261 def set_bug_template(self, val):
262 if type(val) == dict:
263 setattr(self, 'bug_template', val)
264
265
Dan Shiec1d47d2015-02-13 11:38:13 -0800266 def set_require_ssp(self, val):
267 self._set_bool('require_ssp', val)
268
269
Michael Tang6dc174e2016-05-31 23:13:42 -0700270 def set_build(self, val):
271 self._set_string('build', val)
272
273
274 def set_builds(self, val):
275 if type(val) == dict:
276 setattr(self, 'builds', val)
277
Shuqian Zhao13248c52015-03-27 13:32:17 -0700278 def set_attributes(self, val):
Shuqian Zhao9e681212015-09-18 16:11:07 -0700279 # Add subsystem:default if subsystem is not specified.
Shuqian Zhao13248c52015-03-27 13:32:17 -0700280 self._set_set('attributes', val)
Shuqian Zhao9e681212015-09-18 16:11:07 -0700281 if not any(a.startswith('subsystem') for a in self.attributes):
282 self.attributes.add('subsystem:default')
Shuqian Zhao13248c52015-03-27 13:32:17 -0700283
284
Prashanth Bee707312014-03-31 13:00:36 -0700285def _extract_const(expr):
286 assert(expr.__class__ == compiler.ast.Const)
287 assert(expr.value.__class__ in (str, int, float, unicode))
288 return str(expr.value).strip()
289
290
291def _extract_dict(expr):
292 assert(expr.__class__ == compiler.ast.Dict)
293 assert(expr.items.__class__ == list)
294 cf_dict = {}
295 for key, value in expr.items:
296 try:
297 key = _extract_const(key)
298 val = _extract_expression(value)
299 except (AssertionError, ValueError):
300 pass
301 else:
302 cf_dict[key] = val
303 return cf_dict
304
305
306def _extract_list(expr):
307 assert(expr.__class__ == compiler.ast.List)
308 list_values = []
309 for value in expr.nodes:
310 try:
311 list_values.append(_extract_expression(value))
312 except (AssertionError, ValueError):
313 pass
314 return list_values
315
316
317def _extract_name(expr):
318 assert(expr.__class__ == compiler.ast.Name)
319 assert(expr.name in ('False', 'True', 'None'))
320 return str(expr.name)
321
322
323def _extract_expression(expr):
324 if expr.__class__ == compiler.ast.Const:
325 return _extract_const(expr)
326 if expr.__class__ == compiler.ast.Name:
327 return _extract_name(expr)
328 if expr.__class__ == compiler.ast.Dict:
329 return _extract_dict(expr)
330 if expr.__class__ == compiler.ast.List:
331 return _extract_list(expr)
332 raise ValueError('Unknown rval %s' % expr)
333
334
335def _extract_assignment(n):
mbligh93f42092008-07-18 01:01:58 +0000336 assert(n.__class__ == compiler.ast.Assign)
mbligh93f42092008-07-18 01:01:58 +0000337 assert(n.nodes.__class__ == list)
338 assert(len(n.nodes) == 1)
339 assert(n.nodes[0].__class__ == compiler.ast.AssName)
340 assert(n.nodes[0].flags.__class__ == str)
341 assert(n.nodes[0].name.__class__ == str)
342
Prashanth Bee707312014-03-31 13:00:36 -0700343 val = _extract_expression(n.expr)
mbligh93f42092008-07-18 01:01:58 +0000344 key = n.nodes[0].name.lower()
mbligh93f42092008-07-18 01:01:58 +0000345
346 return (key, val)
347
348
Christopher Wiley10439d82016-03-07 12:45:26 -0800349def parse_control_string(control, raise_warnings=False, path=''):
350 """Parse a control file from a string.
351
352 @param control: string containing the text of a control file.
353 @param raise_warnings: True iff ControlData should raise an error on
354 warnings about control file contents.
355 @param path: string path to the control file.
356
357 """
Chris Masone6fed6462011-10-20 16:36:43 -0700358 try:
359 mod = compiler.parse(control)
360 except SyntaxError, e:
361 raise ControlVariableException("Error parsing data because %s" % e)
Christopher Wiley10439d82016-03-07 12:45:26 -0800362 return finish_parse(mod, path, raise_warnings)
Chris Masone6fed6462011-10-20 16:36:43 -0700363
364
mbligh99c2c6f2008-07-11 18:15:46 +0000365def parse_control(path, raise_warnings=False):
mblighd7cd9832008-10-02 16:20:37 +0000366 try:
367 mod = compiler.parseFile(path)
368 except SyntaxError, e:
mbligh4b5c31e2009-07-11 00:55:34 +0000369 raise ControlVariableException("Error parsing %s because %s" %
370 (path, e))
Chris Masone6fed6462011-10-20 16:36:43 -0700371 return finish_parse(mod, path, raise_warnings)
mbligh99c2c6f2008-07-11 18:15:46 +0000372
Chris Masone6fed6462011-10-20 16:36:43 -0700373
374def finish_parse(mod, path, raise_warnings):
mbligh99c2c6f2008-07-11 18:15:46 +0000375 assert(mod.__class__ == compiler.ast.Module)
376 assert(mod.node.__class__ == compiler.ast.Stmt)
377 assert(mod.node.nodes.__class__ == list)
378
379 vars = {}
380 for n in mod.node.nodes:
Prashanth Bee707312014-03-31 13:00:36 -0700381 try:
382 key, val = _extract_assignment(n)
383 vars[key] = val
384 except (AssertionError, ValueError):
385 pass
mblighd7cd9832008-10-02 16:20:37 +0000386 return ControlData(vars, path, raise_warnings)