Home | History | Annotate | Download | only in common_lib
      1 # pylint: disable-msg=C0111
      2 # Copyright 2008 Google Inc. Released under the GPL v2
      3 
      4 import warnings
      5 with 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
     10 import logging
     11 import textwrap
     12 import re
     13 
     14 from autotest_lib.client.common_lib import enum
     15 from autotest_lib.client.common_lib import global_config
     16 
     17 REQUIRED_VARS = set(['author', 'doc', 'name', 'time', 'test_type'])
     18 OBSOLETE_VARS = set(['experimental'])
     19 
     20 CONTROL_TYPE = enum.Enum('Server', 'Client', start_value=1)
     21 CONTROL_TYPE_NAMES =  enum.Enum(*CONTROL_TYPE.names, string_values=True)
     22 
     23 _SUITE_ATTRIBUTE_PREFIX = 'suite:'
     24 
     25 CONFIG = global_config.global_config
     26 
     27 # Default maximum test result size in kB.
     28 DEFAULT_MAX_RESULT_SIZE_KB = CONFIG.get_config_value(
     29         'AUTOSERV', 'default_max_result_size_KB', type=int)
     30 
     31 class ControlVariableException(Exception):
     32     pass
     33 
     34 def _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 
     62 
     63 class ControlData(object):
     64     # 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 
     69     @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 
     84     def __init__(self, vars, path, raise_warnings=False):
     85         # Defaults
     86         self.path = path
     87         self.dependencies = set()
     88         # TODO(jrbarnette): This should be removed once outside
     89         # code that uses can be changed.
     90         self.experimental = False
     91         self.run_verify = True
     92         self.sync_count = 1
     93         self.test_parameters = set()
     94         self.test_category = ''
     95         self.test_class = ''
     96         self.retries = 0
     97         self.job_retries = 0
     98         # 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
    103         self.attributes = set()
    104         self.max_result_size_KB = DEFAULT_MAX_RESULT_SIZE_KB
    105 
    106         _validate_control_file_fields(self.path, vars, raise_warnings)
    107 
    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
    114                 print 'WARNING: %s; skipping' % e
    115 
    116         self._patch_up_suites_from_attributes()
    117 
    118 
    119     @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 
    128     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 
    138     def _patch_up_suites_from_attributes(self):
    139         """Patch up the set of suites this test is part of.
    140 
    141         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
    146         the attributes.
    147 
    148         """
    149 
    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.
    154         if hasattr(self, 'suite'):
    155             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)
    159 
    160         # Figure out if our attributes mention any suites.
    161         for attribute in self.attributes:
    162             if not attribute.startswith(_SUITE_ATTRIBUTE_PREFIX):
    163                 continue
    164             suite_name = attribute[len(_SUITE_ATTRIBUTE_PREFIX):]
    165             suite_names.add(suite_name)
    166 
    167         # Rebuild the suite field if necessary.
    168         if suite_names:
    169             self.set_suite(','.join(sorted(list(suite_names))))
    170 
    171 
    172     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)
    211         items = [x.strip() for x in val.split(',') if x.strip()]
    212         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 
    227     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 
    239     def set_suite(self, val):
    240         self._set_string('suite', val)
    241 
    242 
    243     def set_time(self, val):
    244         self._set_option('time', val, ControlData.TEST_TIME_LIST)
    245 
    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):
    256         self._set_option('test_type', val, list(CONTROL_TYPE.names))
    257 
    258 
    259     def set_test_parameters(self, val):
    260         self._set_set('test_parameters', val)
    261 
    262 
    263     def set_retries(self, val):
    264         self._set_int('retries', val)
    265 
    266 
    267     def set_job_retries(self, val):
    268         self._set_int('job_retries', val)
    269 
    270 
    271     def set_bug_template(self, val):
    272         if type(val) == dict:
    273             setattr(self, 'bug_template', val)
    274 
    275 
    276     def set_require_ssp(self, val):
    277         self._set_bool('require_ssp', val)
    278 
    279 
    280     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 
    288     def set_max_result_size_kb(self, val):
    289         self._set_int('max_result_size_KB', val)
    290 
    291     def set_attributes(self, val):
    292         # Add subsystem:default if subsystem is not specified.
    293         self._set_set('attributes', val)
    294         if not any(a.startswith('subsystem') for a in self.attributes):
    295             self.attributes.add('subsystem:default')
    296 
    297 
    298 def _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 
    304 def _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 
    319 def _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 
    330 def _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 
    336 def _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 
    348 def _extract_assignment(n):
    349     assert(n.__class__ == compiler.ast.Assign)
    350     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 
    356     val = _extract_expression(n.expr)
    357     key = n.nodes[0].name.lower()
    358 
    359     return (key, val)
    360 
    361 
    362 def 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     """
    371     try:
    372         mod = compiler.parse(control)
    373     except SyntaxError, e:
    374         raise ControlVariableException("Error parsing data because %s" % e)
    375     return finish_parse(mod, path, raise_warnings)
    376 
    377 
    378 def parse_control(path, raise_warnings=False):
    379     try:
    380         mod = compiler.parseFile(path)
    381     except SyntaxError, e:
    382         raise ControlVariableException("Error parsing %s because %s" %
    383                                        (path, e))
    384     return finish_parse(mod, path, raise_warnings)
    385 
    386 
    387 def _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 
    400 def finish_parse(mod, path, raise_warnings):
    401     assert(mod.__class__ == compiler.ast.Module)
    402     assert(mod.node.__class__ == compiler.ast.Stmt)
    403     assert(mod.node.nodes.__class__ == list)
    404 
    405     variables = {}
    406     for n in mod.node.nodes:
    407         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)
    421