1# pylint: disable-msg=C0111
2# Copyright 2008 Google Inc. Released under the GPL v2
3
4import 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
11
12from autotest_lib.client.common_lib import enum
13
14REQUIRED_VARS = set(['author', 'doc', 'name', 'time', 'test_type'])
15OBSOLETE_VARS = set(['experimental'])
16
17CONTROL_TYPE = enum.Enum('Server', 'Client', start_value=1)
18CONTROL_TYPE_NAMES =  enum.Enum(*CONTROL_TYPE.names, string_values=True)
19
20_SUITE_ATTRIBUTE_PREFIX = 'suite:'
21
22class ControlVariableException(Exception):
23    pass
24
25def _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
53
54class ControlData(object):
55    # 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
60    @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
75    def __init__(self, vars, path, raise_warnings=False):
76        # Defaults
77        self.path = path
78        self.dependencies = set()
79        # TODO(jrbarnette): This should be removed once outside
80        # code that uses can be changed.
81        self.experimental = False
82        self.run_verify = True
83        self.sync_count = 1
84        self.test_parameters = set()
85        self.test_category = ''
86        self.test_class = ''
87        self.retries = 0
88        self.job_retries = 0
89        # 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
94        self.attributes = set()
95
96        _validate_control_file_fields(self.path, vars, raise_warnings)
97
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
104                print 'WARNING: %s; skipping' % e
105
106        self._patch_up_suites_from_attributes()
107
108
109    @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
118    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
128    def _patch_up_suites_from_attributes(self):
129        """Patch up the set of suites this test is part of.
130
131        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
136        the attributes.
137
138        """
139
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.
144        if hasattr(self, 'suite'):
145            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)
149
150        # Figure out if our attributes mention any suites.
151        for attribute in self.attributes:
152            if not attribute.startswith(_SUITE_ATTRIBUTE_PREFIX):
153                continue
154            suite_name = attribute[len(_SUITE_ATTRIBUTE_PREFIX):]
155            suite_names.add(suite_name)
156
157        # Rebuild the suite field if necessary.
158        if suite_names:
159            self.set_suite(','.join(sorted(list(suite_names))))
160
161
162    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)
201        items = [x.strip() for x in val.split(',') if x.strip()]
202        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
217    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
229    def set_suite(self, val):
230        self._set_string('suite', val)
231
232
233    def set_time(self, val):
234        self._set_option('time', val, ControlData.TEST_TIME_LIST)
235
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):
246        self._set_option('test_type', val, list(CONTROL_TYPE.names))
247
248
249    def set_test_parameters(self, val):
250        self._set_set('test_parameters', val)
251
252
253    def set_retries(self, val):
254        self._set_int('retries', val)
255
256
257    def set_job_retries(self, val):
258        self._set_int('job_retries', val)
259
260
261    def set_bug_template(self, val):
262        if type(val) == dict:
263            setattr(self, 'bug_template', val)
264
265
266    def set_require_ssp(self, val):
267        self._set_bool('require_ssp', val)
268
269
270    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
278    def set_attributes(self, val):
279        # Add subsystem:default if subsystem is not specified.
280        self._set_set('attributes', val)
281        if not any(a.startswith('subsystem') for a in self.attributes):
282            self.attributes.add('subsystem:default')
283
284
285def _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):
336    assert(n.__class__ == compiler.ast.Assign)
337    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
343    val = _extract_expression(n.expr)
344    key = n.nodes[0].name.lower()
345
346    return (key, val)
347
348
349def 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    """
358    try:
359        mod = compiler.parse(control)
360    except SyntaxError, e:
361        raise ControlVariableException("Error parsing data because %s" % e)
362    return finish_parse(mod, path, raise_warnings)
363
364
365def parse_control(path, raise_warnings=False):
366    try:
367        mod = compiler.parseFile(path)
368    except SyntaxError, e:
369        raise ControlVariableException("Error parsing %s because %s" %
370                                       (path, e))
371    return finish_parse(mod, path, raise_warnings)
372
373
374def finish_parse(mod, path, raise_warnings):
375    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:
381        try:
382            key, val = _extract_assignment(n)
383            vars[key] = val
384        except (AssertionError, ValueError):
385            pass
386    return ControlData(vars, path, raise_warnings)
387