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