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 11import textwrap 12import re 13 14from autotest_lib.client.common_lib import enum 15from autotest_lib.client.common_lib import global_config 16from autotest_lib.client.common_lib import priorities 17 18REQUIRED_VARS = set(['author', 'doc', 'name', 'time', 'test_type']) 19OBSOLETE_VARS = set(['experimental']) 20 21CONTROL_TYPE = enum.Enum('Server', 'Client', start_value=1) 22CONTROL_TYPE_NAMES = enum.Enum(*CONTROL_TYPE.names, string_values=True) 23 24_SUITE_ATTRIBUTE_PREFIX = 'suite:' 25 26CONFIG = global_config.global_config 27 28# Default maximum test result size in kB. 29DEFAULT_MAX_RESULT_SIZE_KB = CONFIG.get_config_value( 30 'AUTOSERV', 'default_max_result_size_KB', type=int, default=20000) 31 32 33class ControlVariableException(Exception): 34 pass 35 36def _validate_control_file_fields(control_file_path, control_file_vars, 37 raise_warnings): 38 """Validate the given set of variables from a control file. 39 40 @param control_file_path: string path of the control file these were 41 loaded from. 42 @param control_file_vars: dict of variables set in a control file. 43 @param raise_warnings: True iff we should raise on invalid variables. 44 45 """ 46 diff = REQUIRED_VARS - set(control_file_vars) 47 if diff: 48 warning = ('WARNING: Not all required control ' 49 'variables were specified in %s. Please define ' 50 '%s.') % (control_file_path, ', '.join(diff)) 51 if raise_warnings: 52 raise ControlVariableException(warning) 53 print textwrap.wrap(warning, 80) 54 55 obsolete = OBSOLETE_VARS & set(control_file_vars) 56 if obsolete: 57 warning = ('WARNING: Obsolete variables were ' 58 'specified in %s. Please remove ' 59 '%s.') % (control_file_path, ', '.join(obsolete)) 60 if raise_warnings: 61 raise ControlVariableException(warning) 62 print textwrap.wrap(warning, 80) 63 64 65class ControlData(object): 66 # Available TIME settings in control file, the list must be in lower case 67 # and in ascending order, test running faster comes first. 68 TEST_TIME_LIST = ['fast', 'short', 'medium', 'long', 'lengthy'] 69 TEST_TIME = enum.Enum(*TEST_TIME_LIST, string_values=False) 70 71 @staticmethod 72 def get_test_time_index(time): 73 """ 74 Get the order of estimated test time, based on the TIME setting in 75 Control file. Faster test gets a lower index number. 76 """ 77 try: 78 return ControlData.TEST_TIME.get_value(time.lower()) 79 except AttributeError: 80 # Raise exception if time value is not a valid TIME setting. 81 error_msg = '%s is not a valid TIME.' % time 82 logging.error(error_msg) 83 raise ControlVariableException(error_msg) 84 85 86 def __init__(self, vars, path, raise_warnings=False): 87 # Defaults 88 self.path = path 89 self.dependencies = set() 90 # TODO(jrbarnette): This should be removed once outside 91 # code that uses can be changed. 92 self.experimental = False 93 self.run_verify = True 94 self.sync_count = 1 95 self.test_parameters = set() 96 self.test_category = '' 97 self.test_class = '' 98 self.retries = 0 99 self.job_retries = 0 100 # Default to require server-side package. Unless require_ssp is 101 # explicitly set to False, server-side package will be used for the 102 # job. This can be overridden by global config 103 # AUTOSERV/enable_ssp_container 104 self.require_ssp = None 105 self.attributes = set() 106 self.max_result_size_KB = DEFAULT_MAX_RESULT_SIZE_KB 107 self.priority = priorities.Priority.DEFAULT 108 self.fast = False 109 110 _validate_control_file_fields(self.path, vars, raise_warnings) 111 112 for key, val in vars.iteritems(): 113 try: 114 self.set_attr(key, val, raise_warnings) 115 except Exception, e: 116 if raise_warnings: 117 raise 118 print 'WARNING: %s; skipping' % e 119 120 self._patch_up_suites_from_attributes() 121 122 123 @property 124 def suite_tag_parts(self): 125 """Return the part strings of the test's suite tag.""" 126 if hasattr(self, 'suite'): 127 return [part.strip() for part in self.suite.split(',')] 128 else: 129 return [] 130 131 132 def set_attr(self, attr, val, raise_warnings=False): 133 attr = attr.lower() 134 try: 135 set_fn = getattr(self, 'set_%s' % attr) 136 set_fn(val) 137 except AttributeError: 138 # This must not be a variable we care about 139 pass 140 141 142 def _patch_up_suites_from_attributes(self): 143 """Patch up the set of suites this test is part of. 144 145 Legacy builds will not have an appropriate ATTRIBUTES field set. 146 Take the union of suites specified via ATTRIBUTES and suites specified 147 via SUITE. 148 149 SUITE used to be its own variable, but now suites are taken only from 150 the attributes. 151 152 """ 153 154 suite_names = set() 155 # Extract any suites we know ourselves to be in based on the SUITE 156 # line. This line is deprecated, but control files in old builds will 157 # still have it. 158 if hasattr(self, 'suite'): 159 existing_suites = self.suite.split(',') 160 existing_suites = [name.strip() for name in existing_suites] 161 existing_suites = [name for name in existing_suites if name] 162 suite_names.update(existing_suites) 163 164 # Figure out if our attributes mention any suites. 165 for attribute in self.attributes: 166 if not attribute.startswith(_SUITE_ATTRIBUTE_PREFIX): 167 continue 168 suite_name = attribute[len(_SUITE_ATTRIBUTE_PREFIX):] 169 suite_names.add(suite_name) 170 171 # Rebuild the suite field if necessary. 172 if suite_names: 173 self.set_suite(','.join(sorted(list(suite_names)))) 174 175 176 def _set_string(self, attr, val): 177 val = str(val) 178 setattr(self, attr, val) 179 180 181 def _set_option(self, attr, val, options): 182 val = str(val) 183 if val.lower() not in [x.lower() for x in options]: 184 raise ValueError("%s must be one of the following " 185 "options: %s" % (attr, 186 ', '.join(options))) 187 setattr(self, attr, val) 188 189 190 def _set_bool(self, attr, val): 191 val = str(val).lower() 192 if val == "false": 193 val = False 194 elif val == "true": 195 val = True 196 else: 197 msg = "%s must be either true or false" % attr 198 raise ValueError(msg) 199 setattr(self, attr, val) 200 201 202 def _set_int(self, attr, val, min=None, max=None): 203 val = int(val) 204 if min is not None and min > val: 205 raise ValueError("%s is %d, which is below the " 206 "minimum of %d" % (attr, val, min)) 207 if max is not None and max < val: 208 raise ValueError("%s is %d, which is above the " 209 "maximum of %d" % (attr, val, max)) 210 setattr(self, attr, val) 211 212 213 def _set_set(self, attr, val): 214 val = str(val) 215 items = [x.strip() for x in val.split(',') if x.strip()] 216 setattr(self, attr, set(items)) 217 218 219 def set_author(self, val): 220 self._set_string('author', val) 221 222 223 def set_dependencies(self, val): 224 self._set_set('dependencies', val) 225 226 227 def set_doc(self, val): 228 self._set_string('doc', val) 229 230 231 def set_name(self, val): 232 self._set_string('name', val) 233 234 235 def set_run_verify(self, val): 236 self._set_bool('run_verify', val) 237 238 239 def set_sync_count(self, val): 240 self._set_int('sync_count', val, min=1) 241 242 243 def set_suite(self, val): 244 self._set_string('suite', val) 245 246 247 def set_time(self, val): 248 self._set_option('time', val, ControlData.TEST_TIME_LIST) 249 250 251 def set_test_class(self, val): 252 self._set_string('test_class', val.lower()) 253 254 255 def set_test_category(self, val): 256 self._set_string('test_category', val.lower()) 257 258 259 def set_test_type(self, val): 260 self._set_option('test_type', val, list(CONTROL_TYPE.names)) 261 262 263 def set_test_parameters(self, val): 264 self._set_set('test_parameters', val) 265 266 267 def set_retries(self, val): 268 self._set_int('retries', val) 269 270 271 def set_job_retries(self, val): 272 self._set_int('job_retries', val) 273 274 275 def set_bug_template(self, val): 276 if type(val) == dict: 277 setattr(self, 'bug_template', val) 278 279 280 def set_require_ssp(self, val): 281 self._set_bool('require_ssp', val) 282 283 284 def set_build(self, val): 285 self._set_string('build', val) 286 287 288 def set_builds(self, val): 289 if type(val) == dict: 290 setattr(self, 'builds', val) 291 292 def set_max_result_size_kb(self, val): 293 self._set_int('max_result_size_KB', val) 294 295 def set_priority(self, val): 296 self._set_int('priority', val) 297 298 def set_fast(self, val): 299 self._set_bool('fast', val) 300 301 def set_attributes(self, val): 302 # Add subsystem:default if subsystem is not specified. 303 self._set_set('attributes', val) 304 if not any(a.startswith('subsystem') for a in self.attributes): 305 self.attributes.add('subsystem:default') 306 307 308def _extract_const(expr): 309 assert(expr.__class__ == compiler.ast.Const) 310 assert(expr.value.__class__ in (str, int, float, unicode)) 311 return str(expr.value).strip() 312 313 314def _extract_dict(expr): 315 assert(expr.__class__ == compiler.ast.Dict) 316 assert(expr.items.__class__ == list) 317 cf_dict = {} 318 for key, value in expr.items: 319 try: 320 key = _extract_const(key) 321 val = _extract_expression(value) 322 except (AssertionError, ValueError): 323 pass 324 else: 325 cf_dict[key] = val 326 return cf_dict 327 328 329def _extract_list(expr): 330 assert(expr.__class__ == compiler.ast.List) 331 list_values = [] 332 for value in expr.nodes: 333 try: 334 list_values.append(_extract_expression(value)) 335 except (AssertionError, ValueError): 336 pass 337 return list_values 338 339 340def _extract_name(expr): 341 assert(expr.__class__ == compiler.ast.Name) 342 assert(expr.name in ('False', 'True', 'None')) 343 return str(expr.name) 344 345 346def _extract_expression(expr): 347 if expr.__class__ == compiler.ast.Const: 348 return _extract_const(expr) 349 if expr.__class__ == compiler.ast.Name: 350 return _extract_name(expr) 351 if expr.__class__ == compiler.ast.Dict: 352 return _extract_dict(expr) 353 if expr.__class__ == compiler.ast.List: 354 return _extract_list(expr) 355 raise ValueError('Unknown rval %s' % expr) 356 357 358def _extract_assignment(n): 359 assert(n.__class__ == compiler.ast.Assign) 360 assert(n.nodes.__class__ == list) 361 assert(len(n.nodes) == 1) 362 assert(n.nodes[0].__class__ == compiler.ast.AssName) 363 assert(n.nodes[0].flags.__class__ == str) 364 assert(n.nodes[0].name.__class__ == str) 365 366 val = _extract_expression(n.expr) 367 key = n.nodes[0].name.lower() 368 369 return (key, val) 370 371 372def parse_control_string(control, raise_warnings=False, path=''): 373 """Parse a control file from a string. 374 375 @param control: string containing the text of a control file. 376 @param raise_warnings: True iff ControlData should raise an error on 377 warnings about control file contents. 378 @param path: string path to the control file. 379 380 """ 381 try: 382 mod = compiler.parse(control) 383 except SyntaxError as e: 384 logging.error('Syntax error (%s) while parsing control string:', e) 385 lines = control.split('\n') 386 for n, l in enumerate(lines): 387 logging.error('Line %d: %s', n + 1, l) 388 raise ControlVariableException("Error parsing data because %s" % e) 389 return finish_parse(mod, path, raise_warnings) 390 391 392def parse_control(path, raise_warnings=False): 393 try: 394 mod = compiler.parseFile(path) 395 except SyntaxError, e: 396 raise ControlVariableException("Error parsing %s because %s" % 397 (path, e)) 398 return finish_parse(mod, path, raise_warnings) 399 400 401def _try_extract_assignment(node, variables): 402 """Try to extract assignment from the given node. 403 404 @param node: An Assign object. 405 @param variables: Dictionary to store the parsed assignments. 406 """ 407 try: 408 key, val = _extract_assignment(node) 409 variables[key] = val 410 except (AssertionError, ValueError): 411 pass 412 413 414def finish_parse(mod, path, raise_warnings): 415 assert(mod.__class__ == compiler.ast.Module) 416 assert(mod.node.__class__ == compiler.ast.Stmt) 417 assert(mod.node.nodes.__class__ == list) 418 419 variables = {} 420 injection_variables = {} 421 for n in mod.node.nodes: 422 if (n.__class__ == compiler.ast.Function and 423 re.match('step\d+', n.name)): 424 vars_in_step = {} 425 for sub_node in n.code.nodes: 426 _try_extract_assignment(sub_node, vars_in_step) 427 if vars_in_step: 428 # Empty the vars collection so assignments from multiple steps 429 # won't be mixed. 430 variables.clear() 431 variables.update(vars_in_step) 432 else: 433 _try_extract_assignment(n, injection_variables) 434 435 variables.update(injection_variables) 436 return ControlData(variables, path, raise_warnings) 437