blob: 54d4f4f4d6a3f45af7b9c8e1a056d0ca9d8856f7 [file] [log] [blame]
# Lint as: python2, python3
# pylint: disable-msg=C0111
# Copyright 2008 Google Inc. Released under the GPL v2
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import ast
import logging
import textwrap
import re
import six
from autotest_lib.client.common_lib import autotest_enum
from autotest_lib.client.common_lib import global_config
from autotest_lib.client.common_lib import priorities
REQUIRED_VARS = set(['author', 'doc', 'name', 'time', 'test_type'])
OBSOLETE_VARS = set(['experimental'])
CONTROL_TYPE = autotest_enum.AutotestEnum('Server', 'Client', start_value=1)
CONTROL_TYPE_NAMES = autotest_enum.AutotestEnum(*CONTROL_TYPE.names,
string_values=True)
_SUITE_ATTRIBUTE_PREFIX = 'suite:'
CONFIG = global_config.global_config
# Default maximum test result size in kB.
DEFAULT_MAX_RESULT_SIZE_KB = CONFIG.get_config_value(
'AUTOSERV', 'default_max_result_size_KB', type=int, default=20000)
class ControlVariableException(Exception):
pass
def _validate_control_file_fields(control_file_path, control_file_vars,
raise_warnings):
"""Validate the given set of variables from a control file.
@param control_file_path: string path of the control file these were
loaded from.
@param control_file_vars: dict of variables set in a control file.
@param raise_warnings: True iff we should raise on invalid variables.
"""
diff = REQUIRED_VARS - set(control_file_vars)
if diff:
warning = ('WARNING: Not all required control '
'variables were specified in %s. Please define '
'%s.') % (control_file_path, ', '.join(diff))
if raise_warnings:
raise ControlVariableException(warning)
print(textwrap.wrap(warning, 80))
obsolete = OBSOLETE_VARS & set(control_file_vars)
if obsolete:
warning = ('WARNING: Obsolete variables were '
'specified in %s. Please remove '
'%s.') % (control_file_path, ', '.join(obsolete))
if raise_warnings:
raise ControlVariableException(warning)
print(textwrap.wrap(warning, 80))
class ControlData(object):
# Available TIME settings in control file, the list must be in lower case
# and in ascending order, test running faster comes first.
TEST_TIME_LIST = ['fast', 'short', 'medium', 'long', 'lengthy']
TEST_TIME = autotest_enum.AutotestEnum(*TEST_TIME_LIST,
string_values=False)
@staticmethod
def get_test_time_index(time):
"""
Get the order of estimated test time, based on the TIME setting in
Control file. Faster test gets a lower index number.
"""
try:
return ControlData.TEST_TIME.get_value(time.lower())
except AttributeError:
# Raise exception if time value is not a valid TIME setting.
error_msg = '%s is not a valid TIME.' % time
logging.error(error_msg)
raise ControlVariableException(error_msg)
def __init__(self, vars, path, raise_warnings=False):
# Defaults
self.path = path
self.dependencies = set()
# TODO(jrbarnette): This should be removed once outside
# code that uses can be changed.
self.experimental = False
self.run_verify = True
self.sync_count = 1
self.test_parameters = set()
self.test_category = ''
self.test_class = ''
self.job_retries = 0
# Default to require server-side package. Unless require_ssp is
# explicitly set to False, server-side package will be used for the
# job.
self.require_ssp = None
self.attributes = set()
self.max_result_size_KB = DEFAULT_MAX_RESULT_SIZE_KB
self.priority = priorities.Priority.DEFAULT
self.fast = False
_validate_control_file_fields(self.path, vars, raise_warnings)
for key, val in six.iteritems(vars):
try:
self.set_attr(key, val, raise_warnings)
except Exception as e:
if raise_warnings:
raise
print('WARNING: %s; skipping' % e)
self._patch_up_suites_from_attributes()
@property
def suite_tag_parts(self):
"""Return the part strings of the test's suite tag."""
if hasattr(self, 'suite'):
return [part.strip() for part in self.suite.split(',')]
else:
return []
def set_attr(self, attr, val, raise_warnings=False):
attr = attr.lower()
try:
set_fn = getattr(self, 'set_%s' % attr)
set_fn(val)
except AttributeError:
# This must not be a variable we care about
pass
def _patch_up_suites_from_attributes(self):
"""Patch up the set of suites this test is part of.
Legacy builds will not have an appropriate ATTRIBUTES field set.
Take the union of suites specified via ATTRIBUTES and suites specified
via SUITE.
SUITE used to be its own variable, but now suites are taken only from
the attributes.
"""
suite_names = set()
# Extract any suites we know ourselves to be in based on the SUITE
# line. This line is deprecated, but control files in old builds will
# still have it.
if hasattr(self, 'suite'):
existing_suites = self.suite.split(',')
existing_suites = [name.strip() for name in existing_suites]
existing_suites = [name for name in existing_suites if name]
suite_names.update(existing_suites)
# Figure out if our attributes mention any suites.
for attribute in self.attributes:
if not attribute.startswith(_SUITE_ATTRIBUTE_PREFIX):
continue
suite_name = attribute[len(_SUITE_ATTRIBUTE_PREFIX):]
suite_names.add(suite_name)
# Rebuild the suite field if necessary.
if suite_names:
self.set_suite(','.join(sorted(list(suite_names))))
def _set_string(self, attr, val):
val = str(val)
setattr(self, attr, val)
def _set_option(self, attr, val, options):
val = str(val)
if val.lower() not in [x.lower() for x in options]:
raise ValueError("%s must be one of the following "
"options: %s" % (attr,
', '.join(options)))
setattr(self, attr, val)
def _set_bool(self, attr, val):
val = str(val).lower()
if val == "false":
val = False
elif val == "true":
val = True
else:
msg = "%s must be either true or false" % attr
raise ValueError(msg)
setattr(self, attr, val)
def _set_int(self, attr, val, min=None, max=None):
val = int(val)
if min is not None and min > val:
raise ValueError("%s is %d, which is below the "
"minimum of %d" % (attr, val, min))
if max is not None and max < val:
raise ValueError("%s is %d, which is above the "
"maximum of %d" % (attr, val, max))
setattr(self, attr, val)
def _set_set(self, attr, val):
val = str(val)
items = [x.strip() for x in val.split(',') if x.strip()]
setattr(self, attr, set(items))
def set_author(self, val):
self._set_string('author', val)
def set_dependencies(self, val):
self._set_set('dependencies', val)
def set_doc(self, val):
self._set_string('doc', val)
def set_name(self, val):
self._set_string('name', val)
def set_run_verify(self, val):
self._set_bool('run_verify', val)
def set_sync_count(self, val):
self._set_int('sync_count', val, min=1)
def set_suite(self, val):
self._set_string('suite', val)
def set_time(self, val):
self._set_option('time', val, ControlData.TEST_TIME_LIST)
def set_test_class(self, val):
self._set_string('test_class', val.lower())
def set_test_category(self, val):
self._set_string('test_category', val.lower())
def set_test_type(self, val):
self._set_option('test_type', val, list(CONTROL_TYPE.names))
def set_test_parameters(self, val):
self._set_set('test_parameters', val)
def set_job_retries(self, val):
self._set_int('job_retries', val)
def set_bug_template(self, val):
if type(val) == dict:
setattr(self, 'bug_template', val)
def set_require_ssp(self, val):
self._set_bool('require_ssp', val)
def set_build(self, val):
self._set_string('build', val)
def set_builds(self, val):
if type(val) == dict:
setattr(self, 'builds', val)
def set_max_result_size_kb(self, val):
self._set_int('max_result_size_KB', val)
def set_priority(self, val):
self._set_int('priority', val)
def set_fast(self, val):
self._set_bool('fast', val)
def set_update_type(self, val):
self._set_string('update_type', val)
def set_source_release(self, val):
self._set_string('source_release', val)
def set_target_release(self, val):
self._set_string('target_release', val)
def set_target_payload_uri(self, val):
self._set_string('target_payload_uri', val)
def set_source_payload_uri(self, val):
self._set_string('source_payload_uri', val)
def set_source_archive_uri(self, val):
self._set_string('source_archive_uri', val)
def set_attributes(self, val):
self._set_set('attributes', val)
def _extract_const(expr):
assert (expr.__class__ == ast.Str)
if six.PY2:
assert (expr.s.__class__ in (str, int, float, unicode))
else:
assert (expr.s.__class__ in (str, int, float))
return str(expr.s).strip()
def _extract_dict(expr):
assert (expr.__class__ == ast.Dict)
assert (expr.keys.__class__ == list)
cf_dict = {}
for key, value in zip(expr.keys, expr.values):
try:
key = _extract_const(key)
val = _extract_expression(value)
except (AssertionError, ValueError):
pass
else:
cf_dict[key] = val
return cf_dict
def _extract_list(expr):
assert (expr.__class__ == ast.List)
list_values = []
for value in expr.elts:
try:
list_values.append(_extract_expression(value))
except (AssertionError, ValueError):
pass
return list_values
def _extract_name(expr):
assert (expr.__class__ == ast.Name)
assert (expr.id in ('False', 'True', 'None'))
return str(expr.id)
def _extract_expression(expr):
if expr.__class__ == ast.Str:
return _extract_const(expr)
if expr.__class__ == ast.Name:
return _extract_name(expr)
if expr.__class__ == ast.Dict:
return _extract_dict(expr)
if expr.__class__ == ast.List:
return _extract_list(expr)
if expr.__class__ == ast.Num:
return expr.n
if six.PY3 and expr.__class__ == ast.NameConstant:
return expr.value
if six.PY3 and expr.__class__ == ast.Constant:
try:
return expr.value.strip()
except Exception:
return expr.value
raise ValueError('Unknown rval %s' % expr)
def _extract_assignment(n):
assert (n.__class__ == ast.Assign)
assert (len(n.targets) == 1)
assert (n.targets[0].__class__ == ast.Name)
val = _extract_expression(n.value)
key = n.targets[0].id.lower()
return (key, val)
def parse_control_string(control, raise_warnings=False, path=''):
"""Parse a control file from a string.
@param control: string containing the text of a control file.
@param raise_warnings: True iff ControlData should raise an error on
warnings about control file contents.
@param path: string path to the control file.
"""
try:
mod = ast.parse(control)
except SyntaxError as e:
logging.error('Syntax error (%s) while parsing control string:', e)
lines = control.split('\n')
for n, l in enumerate(lines):
logging.error('Line %d: %s', n + 1, l)
raise ControlVariableException("Error parsing data because %s" % e)
return finish_parse(mod, path, raise_warnings)
def parse_control(path, raise_warnings=False):
try:
with open(path, 'r') as r:
mod = ast.parse(r.read())
except SyntaxError as e:
raise ControlVariableException("Error parsing %s because %s" %
(path, e))
return finish_parse(mod, path, raise_warnings)
def _try_extract_assignment(node, variables):
"""Try to extract assignment from the given node.
@param node: An Assign object.
@param variables: Dictionary to store the parsed assignments.
"""
try:
key, val = _extract_assignment(node)
variables[key] = val
except (AssertionError, ValueError) as e:
pass
def finish_parse(mod, path, raise_warnings):
assert (mod.__class__ == ast.Module)
assert (mod.body.__class__ == list)
variables = {}
injection_variables = {}
for n in mod.body:
if (n.__class__ == ast.FunctionDef and re.match('step\d+', n.name)):
vars_in_step = {}
for sub_node in n.body:
_try_extract_assignment(sub_node, vars_in_step)
if vars_in_step:
# Empty the vars collection so assignments from multiple steps
# won't be mixed.
variables.clear()
variables.update(vars_in_step)
else:
_try_extract_assignment(n, injection_variables)
variables.update(injection_variables)
return ControlData(variables, path, raise_warnings)