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
20class ControlVariableException(Exception):
21    pass
22
23
24class ControlData(object):
25    # Available TIME settings in control file, the list must be in lower case
26    # and in ascending order, test running faster comes first.
27    TEST_TIME_LIST = ['fast', 'short', 'medium', 'long', 'lengthy']
28    TEST_TIME = enum.Enum(*TEST_TIME_LIST, string_values=False)
29
30    @staticmethod
31    def get_test_time_index(time):
32        """
33        Get the order of estimated test time, based on the TIME setting in
34        Control file. Faster test gets a lower index number.
35        """
36        try:
37            return ControlData.TEST_TIME.get_value(time.lower())
38        except AttributeError:
39            # Raise exception if time value is not a valid TIME setting.
40            error_msg = '%s is not a valid TIME.' % time
41            logging.error(error_msg)
42            raise ControlVariableException(error_msg)
43
44
45    def __init__(self, vars, path, raise_warnings=False):
46        # Defaults
47        self.path = path
48        self.dependencies = set()
49        # TODO(jrbarnette): This should be removed once outside
50        # code that uses can be changed.
51        self.experimental = False
52        self.run_verify = True
53        self.sync_count = 1
54        self.test_parameters = set()
55        self.test_category = ''
56        self.test_class = ''
57        self.retries = 0
58        self.job_retries = 0
59        # Default to require server-side package. Unless require_ssp is
60        # explicitly set to False, server-side package will be used for the
61        # job. This can be overridden by global config
62        # AUTOSERV/enable_ssp_container
63        self.require_ssp = None
64        self.attributes = set()
65
66        diff = REQUIRED_VARS - set(vars)
67        if diff:
68            warning = ('WARNING: Not all required control '
69                       'variables were specified in %s.  Please define '
70                       '%s.') % (self.path, ', '.join(diff))
71            if raise_warnings:
72                raise ControlVariableException(warning)
73            print textwrap.wrap(warning, 80)
74
75        obsolete = OBSOLETE_VARS & set(vars)
76        if obsolete:
77            warning = ('WARNING: Obsolete variables were '
78                       'specified in %s.  Please remove '
79                       '%s.') % (self.path, ', '.join(obsolete))
80            if raise_warnings:
81                raise ControlVariableException(warning)
82            print textwrap.wrap(warning, 80)
83
84        for key, val in vars.iteritems():
85            try:
86                self.set_attr(key, val, raise_warnings)
87            except Exception, e:
88                if raise_warnings:
89                    raise
90                print 'WARNING: %s; skipping' % e
91
92
93    def set_attr(self, attr, val, raise_warnings=False):
94        attr = attr.lower()
95        try:
96            set_fn = getattr(self, 'set_%s' % attr)
97            set_fn(val)
98        except AttributeError:
99            # This must not be a variable we care about
100            pass
101
102
103    def _set_string(self, attr, val):
104        val = str(val)
105        setattr(self, attr, val)
106
107
108    def _set_option(self, attr, val, options):
109        val = str(val)
110        if val.lower() not in [x.lower() for x in options]:
111            raise ValueError("%s must be one of the following "
112                             "options: %s" % (attr,
113                             ', '.join(options)))
114        setattr(self, attr, val)
115
116
117    def _set_bool(self, attr, val):
118        val = str(val).lower()
119        if val == "false":
120            val = False
121        elif val == "true":
122            val = True
123        else:
124            msg = "%s must be either true or false" % attr
125            raise ValueError(msg)
126        setattr(self, attr, val)
127
128
129    def _set_int(self, attr, val, min=None, max=None):
130        val = int(val)
131        if min is not None and min > val:
132            raise ValueError("%s is %d, which is below the "
133                             "minimum of %d" % (attr, val, min))
134        if max is not None and max < val:
135            raise ValueError("%s is %d, which is above the "
136                             "maximum of %d" % (attr, val, max))
137        setattr(self, attr, val)
138
139
140    def _set_set(self, attr, val):
141        val = str(val)
142        items = [x.strip() for x in val.split(',')]
143        setattr(self, attr, set(items))
144
145
146    def set_author(self, val):
147        self._set_string('author', val)
148
149
150    def set_dependencies(self, val):
151        self._set_set('dependencies', val)
152
153
154    def set_doc(self, val):
155        self._set_string('doc', val)
156
157
158    def set_name(self, val):
159        self._set_string('name', val)
160
161
162    def set_run_verify(self, val):
163        self._set_bool('run_verify', val)
164
165
166    def set_sync_count(self, val):
167        self._set_int('sync_count', val, min=1)
168
169
170    def set_suite(self, val):
171        self._set_string('suite', val)
172
173
174    def set_time(self, val):
175        self._set_option('time', val, ControlData.TEST_TIME_LIST)
176
177
178    def set_test_class(self, val):
179        self._set_string('test_class', val.lower())
180
181
182    def set_test_category(self, val):
183        self._set_string('test_category', val.lower())
184
185
186    def set_test_type(self, val):
187        self._set_option('test_type', val, list(CONTROL_TYPE.names))
188
189
190    def set_test_parameters(self, val):
191        self._set_set('test_parameters', val)
192
193
194    def set_retries(self, val):
195        self._set_int('retries', val)
196
197
198    def set_job_retries(self, val):
199        self._set_int('job_retries', val)
200
201
202    def set_bug_template(self, val):
203        if type(val) == dict:
204            setattr(self, 'bug_template', val)
205
206
207    def set_require_ssp(self, val):
208        self._set_bool('require_ssp', val)
209
210
211    def set_attributes(self, val):
212        # Add subsystem:default if subsystem is not specified.
213        self._set_set('attributes', val)
214        if not any(a.startswith('subsystem') for a in self.attributes):
215            self.attributes.add('subsystem:default')
216
217
218def _extract_const(expr):
219    assert(expr.__class__ == compiler.ast.Const)
220    assert(expr.value.__class__ in (str, int, float, unicode))
221    return str(expr.value).strip()
222
223
224def _extract_dict(expr):
225    assert(expr.__class__ == compiler.ast.Dict)
226    assert(expr.items.__class__ == list)
227    cf_dict = {}
228    for key, value in expr.items:
229        try:
230            key = _extract_const(key)
231            val = _extract_expression(value)
232        except (AssertionError, ValueError):
233            pass
234        else:
235            cf_dict[key] = val
236    return cf_dict
237
238
239def _extract_list(expr):
240    assert(expr.__class__ == compiler.ast.List)
241    list_values = []
242    for value in expr.nodes:
243        try:
244            list_values.append(_extract_expression(value))
245        except (AssertionError, ValueError):
246            pass
247    return list_values
248
249
250def _extract_name(expr):
251    assert(expr.__class__ == compiler.ast.Name)
252    assert(expr.name in ('False', 'True', 'None'))
253    return str(expr.name)
254
255
256def _extract_expression(expr):
257    if expr.__class__ == compiler.ast.Const:
258        return _extract_const(expr)
259    if expr.__class__ == compiler.ast.Name:
260        return _extract_name(expr)
261    if expr.__class__ == compiler.ast.Dict:
262        return _extract_dict(expr)
263    if expr.__class__ == compiler.ast.List:
264        return _extract_list(expr)
265    raise ValueError('Unknown rval %s' % expr)
266
267
268def _extract_assignment(n):
269    assert(n.__class__ == compiler.ast.Assign)
270    assert(n.nodes.__class__ == list)
271    assert(len(n.nodes) == 1)
272    assert(n.nodes[0].__class__ == compiler.ast.AssName)
273    assert(n.nodes[0].flags.__class__ == str)
274    assert(n.nodes[0].name.__class__ == str)
275
276    val = _extract_expression(n.expr)
277    key = n.nodes[0].name.lower()
278
279    return (key, val)
280
281
282def parse_control_string(control, raise_warnings=False):
283    try:
284        mod = compiler.parse(control)
285    except SyntaxError, e:
286        raise ControlVariableException("Error parsing data because %s" % e)
287    return finish_parse(mod, '', raise_warnings)
288
289
290def parse_control(path, raise_warnings=False):
291    try:
292        mod = compiler.parseFile(path)
293    except SyntaxError, e:
294        raise ControlVariableException("Error parsing %s because %s" %
295                                       (path, e))
296    return finish_parse(mod, path, raise_warnings)
297
298
299def finish_parse(mod, path, raise_warnings):
300    assert(mod.__class__ == compiler.ast.Module)
301    assert(mod.node.__class__ == compiler.ast.Stmt)
302    assert(mod.node.nodes.__class__ == list)
303
304    vars = {}
305    for n in mod.node.nodes:
306        try:
307            key, val = _extract_assignment(n)
308            vars[key] = val
309        except (AssertionError, ValueError):
310            pass
311    return ControlData(vars, path, raise_warnings)
312