blob: a195bea50bd78707d7f73fb191441bfb86a194fa [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
Dan Shif53d1262017-06-19 11:25:25 -070010import logging
11import textwrap
12import re
mbligh99c2c6f2008-07-11 18:15:46 +000013
Dan Shief5b53f2013-01-22 10:22:01 -080014from autotest_lib.client.common_lib import enum
Dan Shif53d1262017-06-19 11:25:25 -070015from autotest_lib.client.common_lib import global_config
mbligh99c2c6f2008-07-11 18:15:46 +000016
Scott Zawalski8ad52b62012-10-29 09:59:33 -040017REQUIRED_VARS = set(['author', 'doc', 'name', 'time', 'test_type'])
J. Richard Barnette42384782014-05-09 10:47:24 -070018OBSOLETE_VARS = set(['experimental'])
mbligh99c2c6f2008-07-11 18:15:46 +000019
Aviv Keshet3dd8beb2013-05-13 17:36:04 -070020CONTROL_TYPE = enum.Enum('Server', 'Client', start_value=1)
21CONTROL_TYPE_NAMES = enum.Enum(*CONTROL_TYPE.names, string_values=True)
22
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -080023_SUITE_ATTRIBUTE_PREFIX = 'suite:'
24
Dan Shif53d1262017-06-19 11:25:25 -070025CONFIG = global_config.global_config
26
27# Default maximum test result size in kB.
28DEFAULT_MAX_RESULT_SIZE_KB = CONFIG.get_config_value(
29 'AUTOSERV', 'default_max_result_size_KB', type=int)
30
mbligh99c2c6f2008-07-11 18:15:46 +000031class ControlVariableException(Exception):
32 pass
33
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -080034def _validate_control_file_fields(control_file_path, control_file_vars,
35 raise_warnings):
36 """Validate the given set of variables from a control file.
37
38 @param control_file_path: string path of the control file these were
39 loaded from.
40 @param control_file_vars: dict of variables set in a control file.
41 @param raise_warnings: True iff we should raise on invalid variables.
42
43 """
44 diff = REQUIRED_VARS - set(control_file_vars)
45 if diff:
46 warning = ('WARNING: Not all required control '
47 'variables were specified in %s. Please define '
48 '%s.') % (control_file_path, ', '.join(diff))
49 if raise_warnings:
50 raise ControlVariableException(warning)
51 print textwrap.wrap(warning, 80)
52
53 obsolete = OBSOLETE_VARS & set(control_file_vars)
54 if obsolete:
55 warning = ('WARNING: Obsolete variables were '
56 'specified in %s. Please remove '
57 '%s.') % (control_file_path, ', '.join(obsolete))
58 if raise_warnings:
59 raise ControlVariableException(warning)
60 print textwrap.wrap(warning, 80)
61
mbligh99c2c6f2008-07-11 18:15:46 +000062
63class ControlData(object):
Dan Shief5b53f2013-01-22 10:22:01 -080064 # Available TIME settings in control file, the list must be in lower case
65 # and in ascending order, test running faster comes first.
66 TEST_TIME_LIST = ['fast', 'short', 'medium', 'long', 'lengthy']
67 TEST_TIME = enum.Enum(*TEST_TIME_LIST, string_values=False)
68
Dan Shief5b53f2013-01-22 10:22:01 -080069 @staticmethod
70 def get_test_time_index(time):
71 """
72 Get the order of estimated test time, based on the TIME setting in
73 Control file. Faster test gets a lower index number.
74 """
75 try:
76 return ControlData.TEST_TIME.get_value(time.lower())
77 except AttributeError:
78 # Raise exception if time value is not a valid TIME setting.
79 error_msg = '%s is not a valid TIME.' % time
80 logging.error(error_msg)
81 raise ControlVariableException(error_msg)
82
83
mblighd7cd9832008-10-02 16:20:37 +000084 def __init__(self, vars, path, raise_warnings=False):
mbligh99c2c6f2008-07-11 18:15:46 +000085 # Defaults
mblighd7cd9832008-10-02 16:20:37 +000086 self.path = path
mbligh99c2c6f2008-07-11 18:15:46 +000087 self.dependencies = set()
J. Richard Barnette42384782014-05-09 10:47:24 -070088 # TODO(jrbarnette): This should be removed once outside
89 # code that uses can be changed.
mbligh99c2c6f2008-07-11 18:15:46 +000090 self.experimental = False
91 self.run_verify = True
92 self.sync_count = 1
Eric Li861b2d52011-02-04 14:50:35 -080093 self.test_parameters = set()
Scott Zawalski8ad52b62012-10-29 09:59:33 -040094 self.test_category = ''
95 self.test_class = ''
Aviv Keshet6f455262013-03-01 16:02:29 -080096 self.retries = 0
Fang Denge3bc24b2014-03-17 15:19:46 -070097 self.job_retries = 0
Dan Shiec1d47d2015-02-13 11:38:13 -080098 # Default to require server-side package. Unless require_ssp is
99 # explicitly set to False, server-side package will be used for the
100 # job. This can be overridden by global config
101 # AUTOSERV/enable_ssp_container
102 self.require_ssp = None
Shuqian Zhao13248c52015-03-27 13:32:17 -0700103 self.attributes = set()
Dan Shif53d1262017-06-19 11:25:25 -0700104 self.max_result_size_KB = DEFAULT_MAX_RESULT_SIZE_KB
mbligh99c2c6f2008-07-11 18:15:46 +0000105
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -0800106 _validate_control_file_fields(self.path, vars, raise_warnings)
mbligh99c2c6f2008-07-11 18:15:46 +0000107
108 for key, val in vars.iteritems():
109 try:
110 self.set_attr(key, val, raise_warnings)
111 except Exception, e:
112 if raise_warnings:
113 raise
J. Richard Barnette42384782014-05-09 10:47:24 -0700114 print 'WARNING: %s; skipping' % e
mbligh99c2c6f2008-07-11 18:15:46 +0000115
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -0800116 self._patch_up_suites_from_attributes()
117
mbligh99c2c6f2008-07-11 18:15:46 +0000118
Allen Lif20e17d2017-01-03 18:24:19 -0800119 @property
120 def suite_tag_parts(self):
121 """Return the part strings of the test's suite tag."""
122 if hasattr(self, 'suite'):
123 return [part.strip() for part in self.suite.split(',')]
124 else:
125 return []
126
127
mbligh99c2c6f2008-07-11 18:15:46 +0000128 def set_attr(self, attr, val, raise_warnings=False):
129 attr = attr.lower()
130 try:
131 set_fn = getattr(self, 'set_%s' % attr)
132 set_fn(val)
133 except AttributeError:
134 # This must not be a variable we care about
135 pass
136
137
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -0800138 def _patch_up_suites_from_attributes(self):
139 """Patch up the set of suites this test is part of.
140
Christopher Wileybfec5922016-03-07 09:11:32 -0800141 Legacy builds will not have an appropriate ATTRIBUTES field set.
142 Take the union of suites specified via ATTRIBUTES and suites specified
143 via SUITE.
144
145 SUITE used to be its own variable, but now suites are taken only from
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -0800146 the attributes.
147
148 """
Christopher Wileybfec5922016-03-07 09:11:32 -0800149
150 suite_names = set()
151 # Extract any suites we know ourselves to be in based on the SUITE
152 # line. This line is deprecated, but control files in old builds will
153 # still have it.
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -0800154 if hasattr(self, 'suite'):
Christopher Wileybfec5922016-03-07 09:11:32 -0800155 existing_suites = self.suite.split(',')
156 existing_suites = [name.strip() for name in existing_suites]
157 existing_suites = [name for name in existing_suites if name]
158 suite_names.update(existing_suites)
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -0800159
160 # Figure out if our attributes mention any suites.
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -0800161 for attribute in self.attributes:
162 if not attribute.startswith(_SUITE_ATTRIBUTE_PREFIX):
163 continue
164 suite_name = attribute[len(_SUITE_ATTRIBUTE_PREFIX):]
Christopher Wileybfec5922016-03-07 09:11:32 -0800165 suite_names.add(suite_name)
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -0800166
167 # Rebuild the suite field if necessary.
168 if suite_names:
Christopher Wiley4a140562016-03-08 09:36:24 -0800169 self.set_suite(','.join(sorted(list(suite_names))))
Christopher Wiley6a8ac2d2016-03-01 15:41:36 -0800170
171
mbligh99c2c6f2008-07-11 18:15:46 +0000172 def _set_string(self, attr, val):
173 val = str(val)
174 setattr(self, attr, val)
175
176
177 def _set_option(self, attr, val, options):
178 val = str(val)
179 if val.lower() not in [x.lower() for x in options]:
180 raise ValueError("%s must be one of the following "
181 "options: %s" % (attr,
182 ', '.join(options)))
183 setattr(self, attr, val)
184
185
186 def _set_bool(self, attr, val):
187 val = str(val).lower()
188 if val == "false":
189 val = False
190 elif val == "true":
191 val = True
192 else:
193 msg = "%s must be either true or false" % attr
194 raise ValueError(msg)
195 setattr(self, attr, val)
196
197
198 def _set_int(self, attr, val, min=None, max=None):
199 val = int(val)
200 if min is not None and min > val:
201 raise ValueError("%s is %d, which is below the "
202 "minimum of %d" % (attr, val, min))
203 if max is not None and max < val:
204 raise ValueError("%s is %d, which is above the "
205 "maximum of %d" % (attr, val, max))
206 setattr(self, attr, val)
207
208
209 def _set_set(self, attr, val):
210 val = str(val)
Dan Shicddad392016-03-23 15:15:07 -0700211 items = [x.strip() for x in val.split(',') if x.strip()]
mbligh99c2c6f2008-07-11 18:15:46 +0000212 setattr(self, attr, set(items))
213
214
215 def set_author(self, val):
216 self._set_string('author', val)
217
218
219 def set_dependencies(self, val):
220 self._set_set('dependencies', val)
221
222
223 def set_doc(self, val):
224 self._set_string('doc', val)
225
226
mbligh99c2c6f2008-07-11 18:15:46 +0000227 def set_name(self, val):
228 self._set_string('name', val)
229
230
231 def set_run_verify(self, val):
232 self._set_bool('run_verify', val)
233
234
235 def set_sync_count(self, val):
236 self._set_int('sync_count', val, min=1)
237
238
Chris Masone6fed6462011-10-20 16:36:43 -0700239 def set_suite(self, val):
240 self._set_string('suite', val)
241
242
mbligh99c2c6f2008-07-11 18:15:46 +0000243 def set_time(self, val):
Dan Shief5b53f2013-01-22 10:22:01 -0800244 self._set_option('time', val, ControlData.TEST_TIME_LIST)
mbligh99c2c6f2008-07-11 18:15:46 +0000245
246
247 def set_test_class(self, val):
248 self._set_string('test_class', val.lower())
249
250
251 def set_test_category(self, val):
252 self._set_string('test_category', val.lower())
253
254
255 def set_test_type(self, val):
Aviv Keshet3dd8beb2013-05-13 17:36:04 -0700256 self._set_option('test_type', val, list(CONTROL_TYPE.names))
mbligh99c2c6f2008-07-11 18:15:46 +0000257
Eric Li861b2d52011-02-04 14:50:35 -0800258
Eric Lid3e8a3b2010-12-23 10:02:07 -0800259 def set_test_parameters(self, val):
260 self._set_set('test_parameters', val)
261
mbligh99c2c6f2008-07-11 18:15:46 +0000262
Aviv Keshet6f455262013-03-01 16:02:29 -0800263 def set_retries(self, val):
264 self._set_int('retries', val)
265
Dan Shiec1d47d2015-02-13 11:38:13 -0800266
Fang Denge3bc24b2014-03-17 15:19:46 -0700267 def set_job_retries(self, val):
268 self._set_int('job_retries', val)
269
Aviv Keshet6f455262013-03-01 16:02:29 -0800270
Prashanth Bee707312014-03-31 13:00:36 -0700271 def set_bug_template(self, val):
272 if type(val) == dict:
273 setattr(self, 'bug_template', val)
274
275
Dan Shiec1d47d2015-02-13 11:38:13 -0800276 def set_require_ssp(self, val):
277 self._set_bool('require_ssp', val)
278
279
Michael Tang6dc174e2016-05-31 23:13:42 -0700280 def set_build(self, val):
281 self._set_string('build', val)
282
283
284 def set_builds(self, val):
285 if type(val) == dict:
286 setattr(self, 'builds', val)
287
Dan Shif53d1262017-06-19 11:25:25 -0700288 def set_max_result_size_kb(self, val):
289 self._set_int('max_result_size_KB', val)
290
Shuqian Zhao13248c52015-03-27 13:32:17 -0700291 def set_attributes(self, val):
Shuqian Zhao9e681212015-09-18 16:11:07 -0700292 # Add subsystem:default if subsystem is not specified.
Shuqian Zhao13248c52015-03-27 13:32:17 -0700293 self._set_set('attributes', val)
Shuqian Zhao9e681212015-09-18 16:11:07 -0700294 if not any(a.startswith('subsystem') for a in self.attributes):
295 self.attributes.add('subsystem:default')
Shuqian Zhao13248c52015-03-27 13:32:17 -0700296
297
Prashanth Bee707312014-03-31 13:00:36 -0700298def _extract_const(expr):
299 assert(expr.__class__ == compiler.ast.Const)
300 assert(expr.value.__class__ in (str, int, float, unicode))
301 return str(expr.value).strip()
302
303
304def _extract_dict(expr):
305 assert(expr.__class__ == compiler.ast.Dict)
306 assert(expr.items.__class__ == list)
307 cf_dict = {}
308 for key, value in expr.items:
309 try:
310 key = _extract_const(key)
311 val = _extract_expression(value)
312 except (AssertionError, ValueError):
313 pass
314 else:
315 cf_dict[key] = val
316 return cf_dict
317
318
319def _extract_list(expr):
320 assert(expr.__class__ == compiler.ast.List)
321 list_values = []
322 for value in expr.nodes:
323 try:
324 list_values.append(_extract_expression(value))
325 except (AssertionError, ValueError):
326 pass
327 return list_values
328
329
330def _extract_name(expr):
331 assert(expr.__class__ == compiler.ast.Name)
332 assert(expr.name in ('False', 'True', 'None'))
333 return str(expr.name)
334
335
336def _extract_expression(expr):
337 if expr.__class__ == compiler.ast.Const:
338 return _extract_const(expr)
339 if expr.__class__ == compiler.ast.Name:
340 return _extract_name(expr)
341 if expr.__class__ == compiler.ast.Dict:
342 return _extract_dict(expr)
343 if expr.__class__ == compiler.ast.List:
344 return _extract_list(expr)
345 raise ValueError('Unknown rval %s' % expr)
346
347
348def _extract_assignment(n):
mbligh93f42092008-07-18 01:01:58 +0000349 assert(n.__class__ == compiler.ast.Assign)
mbligh93f42092008-07-18 01:01:58 +0000350 assert(n.nodes.__class__ == list)
351 assert(len(n.nodes) == 1)
352 assert(n.nodes[0].__class__ == compiler.ast.AssName)
353 assert(n.nodes[0].flags.__class__ == str)
354 assert(n.nodes[0].name.__class__ == str)
355
Prashanth Bee707312014-03-31 13:00:36 -0700356 val = _extract_expression(n.expr)
mbligh93f42092008-07-18 01:01:58 +0000357 key = n.nodes[0].name.lower()
mbligh93f42092008-07-18 01:01:58 +0000358
359 return (key, val)
360
361
Christopher Wiley10439d82016-03-07 12:45:26 -0800362def parse_control_string(control, raise_warnings=False, path=''):
363 """Parse a control file from a string.
364
365 @param control: string containing the text of a control file.
366 @param raise_warnings: True iff ControlData should raise an error on
367 warnings about control file contents.
368 @param path: string path to the control file.
369
370 """
Chris Masone6fed6462011-10-20 16:36:43 -0700371 try:
372 mod = compiler.parse(control)
373 except SyntaxError, e:
374 raise ControlVariableException("Error parsing data because %s" % e)
Christopher Wiley10439d82016-03-07 12:45:26 -0800375 return finish_parse(mod, path, raise_warnings)
Chris Masone6fed6462011-10-20 16:36:43 -0700376
377
mbligh99c2c6f2008-07-11 18:15:46 +0000378def parse_control(path, raise_warnings=False):
mblighd7cd9832008-10-02 16:20:37 +0000379 try:
380 mod = compiler.parseFile(path)
381 except SyntaxError, e:
mbligh4b5c31e2009-07-11 00:55:34 +0000382 raise ControlVariableException("Error parsing %s because %s" %
383 (path, e))
Chris Masone6fed6462011-10-20 16:36:43 -0700384 return finish_parse(mod, path, raise_warnings)
mbligh99c2c6f2008-07-11 18:15:46 +0000385
Chris Masone6fed6462011-10-20 16:36:43 -0700386
Dan Shif53d1262017-06-19 11:25:25 -0700387def _try_extract_assignment(node, variables):
388 """Try to extract assignment from the given node.
389
390 @param node: An Assign object.
391 @param variables: Dictionary to store the parsed assignments.
392 """
393 try:
394 key, val = _extract_assignment(node)
395 variables[key] = val
396 except (AssertionError, ValueError):
397 pass
398
399
Chris Masone6fed6462011-10-20 16:36:43 -0700400def finish_parse(mod, path, raise_warnings):
mbligh99c2c6f2008-07-11 18:15:46 +0000401 assert(mod.__class__ == compiler.ast.Module)
402 assert(mod.node.__class__ == compiler.ast.Stmt)
403 assert(mod.node.nodes.__class__ == list)
404
Dan Shif53d1262017-06-19 11:25:25 -0700405 variables = {}
mbligh99c2c6f2008-07-11 18:15:46 +0000406 for n in mod.node.nodes:
Dan Shif53d1262017-06-19 11:25:25 -0700407 if (n.__class__ == compiler.ast.Function and
408 re.match('step\d+', n.name)):
409 vars_in_step = {}
410 for sub_node in n.code.nodes:
411 _try_extract_assignment(sub_node, vars_in_step)
412 if vars_in_step:
413 # Empty the vars collection so assignments from multiple steps
414 # won't be mixed.
415 variables.clear()
416 variables.update(vars_in_step)
417 else:
418 _try_extract_assignment(n, variables)
419
420 return ControlData(variables, path, raise_warnings)