1"""Base support for parser scenario testing. 2""" 3 4from os import path 5import ConfigParser, os, shelve, shutil, sys, tarfile, time 6import difflib, itertools 7import common 8from autotest_lib.client.common_lib import utils, autotemp 9from autotest_lib.tko import parser_lib 10from autotest_lib.tko.parsers.test import templates 11from autotest_lib.tko.parsers.test import unittest_hotfix 12 13TEMPLATES_DIRPATH = templates.__path__[0] 14# Set TZ used to UTC 15os.environ['TZ'] = 'UTC' 16time.tzset() 17 18KEYVAL = 'keyval' 19STATUS_VERSION = 'status_version' 20PARSER_RESULT_STORE = 'parser_result.store' 21RESULTS_DIR_TARBALL = 'results_dir.tgz' 22CONFIG_FILENAME = 'scenario.cfg' 23TEST = 'test' 24PARSER_RESULT_TAG = 'parser_result_tag' 25 26 27class Error(Exception): 28 pass 29 30 31class BadResultsDirectoryError(Error): 32 pass 33 34 35class UnsupportedParserResultError(Error): 36 pass 37 38 39class UnsupportedTemplateTypeError(Error): 40 pass 41 42 43 44class ParserException(object): 45 """Abstract representation of exception raised from parser execution. 46 47 We will want to persist exceptions raised from the parser but also change 48 the objects that make them up during refactor. For this reason 49 we can't merely pickle the original. 50 """ 51 52 def __init__(self, orig): 53 """ 54 Args: 55 orig: Exception; To copy 56 """ 57 self.classname = orig.__class__.__name__ 58 print "Copying exception:", self.classname 59 for key, val in orig.__dict__.iteritems(): 60 setattr(self, key, val) 61 62 63 def __eq__(self, other): 64 """Test if equal to another ParserException.""" 65 return self.__dict__ == other.__dict__ 66 67 68 def __ne__(self, other): 69 """Test if not equal to another ParserException.""" 70 return self.__dict__ != other.__dict__ 71 72 73 def __str__(self): 74 sd = self.__dict__ 75 pairs = ['%s="%s"' % (k, sd[k]) for k in sorted(sd.keys())] 76 return "<%s: %s>" % (self.classname, ', '.join(pairs)) 77 78 79class ParserTestResult(object): 80 """Abstract representation of test result parser state. 81 82 We will want to persist test results but also change the 83 objects that make them up during refactor. For this reason 84 we can't merely pickle the originals. 85 """ 86 87 def __init__(self, orig): 88 """ 89 Tracking all the attributes as they change over time is 90 not desirable. Instead we populate the instance's __dict__ 91 by introspecting orig. 92 93 Args: 94 orig: testobj; Framework test result instance to copy. 95 """ 96 for key, val in orig.__dict__.iteritems(): 97 if key == 'kernel': 98 setattr(self, key, dict(val.__dict__)) 99 elif key == 'iterations': 100 setattr(self, key, [dict(it.__dict__) for it in val]) 101 else: 102 setattr(self, key, val) 103 104 105 def __eq__(self, other): 106 """Test if equal to another ParserTestResult.""" 107 return self.__dict__ == other.__dict__ 108 109 110 def __ne__(self, other): 111 """Test if not equal to another ParserTestResult.""" 112 return self.__dict__ != other.__dict__ 113 114 115 def __str__(self): 116 sd = self.__dict__ 117 pairs = ['%s="%s"' % (k, sd[k]) for k in sorted(sd.keys())] 118 return "<%s: %s>" % (self.__class__.__name__, ', '.join(pairs)) 119 120 121def copy_parser_result(parser_result): 122 """Copy parser_result into ParserTestResult instances. 123 124 Args: 125 parser_result: 126 list; [testobj, ...] 127 - Or - 128 Exception 129 130 Returns: 131 list; [ParserTestResult, ...] 132 - Or - 133 ParserException 134 135 Raises: 136 UnsupportedParserResultError; If parser_result type is not supported 137 """ 138 if type(parser_result) is list: 139 return [ParserTestResult(test) for test in parser_result] 140 elif isinstance(parser_result, Exception): 141 return ParserException(parser_result) 142 else: 143 raise UnsupportedParserResultError 144 145 146def compare_parser_results(left, right): 147 """Generates a textual report (for now) on the differences between. 148 149 Args: 150 left: list of ParserTestResults or a single ParserException 151 right: list of ParserTestResults or a single ParserException 152 153 Returns: Generator returned from difflib.Differ().compare() 154 """ 155 def to_los(obj): 156 """Generate a list of strings representation of object.""" 157 if type(obj) is list: 158 return [ 159 '%d) %s' % pair 160 for pair in itertools.izip(itertools.count(), obj)] 161 else: 162 return ['i) %s' % obj] 163 164 return difflib.Differ().compare(to_los(left), to_los(right)) 165 166 167class ParserHarness(object): 168 """Harness for objects related to the parser. 169 170 This can exercise a parser on specific result data in various ways. 171 """ 172 173 def __init__( 174 self, parser, job, job_keyval, status_version, status_log_filepath): 175 """ 176 Args: 177 parser: tko.parsers.base.parser; Subclass instance of base parser. 178 job: job implementation; Returned from parser.make_job() 179 job_keyval: dict; Result of parsing job keyval file. 180 status_version: str; Status log format version 181 status_log_filepath: str; Path to result data status.log file 182 """ 183 self.parser = parser 184 self.job = job 185 self.job_keyval = job_keyval 186 self.status_version = status_version 187 self.status_log_filepath = status_log_filepath 188 189 190 def execute(self): 191 """Basic exercise, pass entire log data into .end() 192 193 Returns: list; [testobj, ...] 194 """ 195 status_lines = open(self.status_log_filepath).readlines() 196 self.parser.start(self.job) 197 return self.parser.end(status_lines) 198 199 200class BaseScenarioTestCase(unittest_hotfix.TestCase): 201 """Base class for all Scenario TestCase implementations. 202 203 This will load up all resources from scenario package directory upon 204 instantiation, and initialize a new ParserHarness before each test 205 method execution. 206 """ 207 def __init__(self, methodName='runTest'): 208 unittest_hotfix.TestCase.__init__(self, methodName) 209 self.package_dirpath = path.dirname( 210 sys.modules[self.__module__].__file__) 211 self.tmp_dirpath, self.results_dirpath = load_results_dir( 212 self.package_dirpath) 213 self.parser_result_store = load_parser_result_store( 214 self.package_dirpath) 215 self.config = load_config(self.package_dirpath) 216 self.parser_result_tag = self.config.get( 217 TEST, PARSER_RESULT_TAG) 218 self.expected_status_version = self.config.getint( 219 TEST, STATUS_VERSION) 220 self.harness = None 221 222 223 def setUp(self): 224 if self.results_dirpath: 225 self.harness = new_parser_harness(self.results_dirpath) 226 227 228 def tearDown(self): 229 if self.tmp_dirpath: 230 self.tmp_dirpath.clean() 231 232 233 def test_status_version(self): 234 """Ensure basic sanity.""" 235 self.skipIf(not self.harness) 236 self.assertEquals( 237 self.harness.status_version, self.expected_status_version) 238 239 240def shelve_open(filename, flag='c', protocol=None, writeback=False): 241 """A more system-portable wrapper around shelve.open, with the exact 242 same arguments and interpretation.""" 243 import dumbdbm 244 return shelve.Shelf(dumbdbm.open(filename, flag), protocol, writeback) 245 246 247def new_parser_harness(results_dirpath): 248 """Ensure sane environment and create new parser with wrapper. 249 250 Args: 251 results_dirpath: str; Path to job results directory 252 253 Returns: 254 ParserHarness; 255 256 Raises: 257 BadResultsDirectoryError; If results dir does not exist or is malformed. 258 """ 259 if not path.exists(results_dirpath): 260 raise BadResultsDirectoryError 261 262 keyval_path = path.join(results_dirpath, KEYVAL) 263 job_keyval = utils.read_keyval(keyval_path) 264 status_version = job_keyval[STATUS_VERSION] 265 parser = parser_lib.parser(status_version) 266 job = parser.make_job(results_dirpath) 267 status_log_filepath = path.join(results_dirpath, 'status.log') 268 if not path.exists(status_log_filepath): 269 raise BadResultsDirectoryError 270 271 return ParserHarness( 272 parser, job, job_keyval, status_version, status_log_filepath) 273 274 275def store_parser_result(package_dirpath, parser_result, tag): 276 """Persist parser result to specified scenario package, keyed by tag. 277 278 Args: 279 package_dirpath: str; Path to scenario package directory. 280 parser_result: list or Exception; Result from ParserHarness.execute 281 tag: str; Tag to use as shelve key for persisted parser_result 282 """ 283 copy = copy_parser_result(parser_result) 284 sto_filepath = path.join(package_dirpath, PARSER_RESULT_STORE) 285 sto = shelve_open(sto_filepath) 286 sto[tag] = copy 287 sto.close() 288 289 290def load_parser_result_store(package_dirpath, open_for_write=False): 291 """Load parser result store from specified scenario package. 292 293 Args: 294 package_dirpath: str; Path to scenario package directory. 295 open_for_write: bool; Open store for writing. 296 297 Returns: 298 shelve.DbfilenameShelf; Looks and acts like a dict 299 """ 300 open_flag = open_for_write and 'c' or 'r' 301 sto_filepath = path.join(package_dirpath, PARSER_RESULT_STORE) 302 return shelve_open(sto_filepath, flag=open_flag) 303 304 305def store_results_dir(package_dirpath, results_dirpath): 306 """Make tarball of results_dirpath in package_dirpath. 307 308 Args: 309 package_dirpath: str; Path to scenario package directory. 310 results_dirpath: str; Path to job results directory 311 """ 312 tgz_filepath = path.join(package_dirpath, RESULTS_DIR_TARBALL) 313 tgz = tarfile.open(tgz_filepath, 'w:gz') 314 results_dirname = path.basename(results_dirpath) 315 tgz.add(results_dirpath, results_dirname) 316 tgz.close() 317 318 319def load_results_dir(package_dirpath): 320 """Unpack results tarball in package_dirpath to temp dir. 321 322 Args: 323 package_dirpath: str; Path to scenario package directory. 324 325 Returns: 326 str; New temp path for extracted results directory. 327 - Or - 328 None; If tarball does not exist 329 """ 330 tgz_filepath = path.join(package_dirpath, RESULTS_DIR_TARBALL) 331 if not path.exists(tgz_filepath): 332 return None, None 333 334 tgz = tarfile.open(tgz_filepath, 'r:gz') 335 tmp_dirpath = autotemp.tempdir(unique_id='scenario_base') 336 results_dirname = tgz.next().name 337 tgz.extract(results_dirname, tmp_dirpath.name) 338 for info in tgz: 339 tgz.extract(info.name, tmp_dirpath.name) 340 return tmp_dirpath, path.join(tmp_dirpath.name, results_dirname) 341 342 343def write_config(package_dirpath, **properties): 344 """Write test configuration file to package_dirpath. 345 346 Args: 347 package_dirpath: str; Path to scenario package directory. 348 properties: dict; Key value entries to write to to config file. 349 """ 350 config = ConfigParser.RawConfigParser() 351 config.add_section(TEST) 352 for key, val in properties.iteritems(): 353 config.set(TEST, key, val) 354 355 config_filepath = path.join(package_dirpath, CONFIG_FILENAME) 356 fi = open(config_filepath, 'w') 357 config.write(fi) 358 fi.close() 359 360 361def load_config(package_dirpath): 362 """Load config from package_dirpath. 363 364 Args: 365 package_dirpath: str; Path to scenario package directory. 366 367 Returns: 368 ConfigParser.RawConfigParser; 369 """ 370 config = ConfigParser.RawConfigParser() 371 config_filepath = path.join(package_dirpath, CONFIG_FILENAME) 372 config.read(config_filepath) 373 return config 374 375 376def install_unittest_module(package_dirpath, template_type): 377 """Install specified unittest template module to package_dirpath. 378 379 Template modules are stored in tko/parsers/test/templates. 380 Installation includes: 381 Copying to package_dirpath/template_type_unittest.py 382 Copying scenario package common.py to package_dirpath 383 Touching package_dirpath/__init__.py 384 385 Args: 386 package_dirpath: str; Path to scenario package directory. 387 template_type: str; Name of template module to install. 388 389 Raises: 390 UnsupportedTemplateTypeError; If there is no module in 391 templates package called template_type. 392 """ 393 from_filepath = path.join( 394 TEMPLATES_DIRPATH, '%s.py' % template_type) 395 if not path.exists(from_filepath): 396 raise UnsupportedTemplateTypeError 397 398 to_filepath = path.join( 399 package_dirpath, '%s_unittest.py' % template_type) 400 shutil.copy(from_filepath, to_filepath) 401 402 # For convenience we must copy the common.py hack file too :-( 403 from_common_filepath = path.join( 404 TEMPLATES_DIRPATH, 'scenario_package_common.py') 405 to_common_filepath = path.join(package_dirpath, 'common.py') 406 shutil.copy(from_common_filepath, to_common_filepath) 407 408 # And last but not least, touch an __init__ file 409 os.mknod(path.join(package_dirpath, '__init__.py')) 410 411 412def fix_package_dirname(package_dirname): 413 """Convert package_dirname to a valid package name string, if necessary. 414 415 Args: 416 package_dirname: str; Name of scenario package directory. 417 418 Returns: 419 str; Possibly fixed package_dirname 420 """ 421 # Really stupid atm, just enough to handle results dirnames 422 package_dirname = package_dirname.replace('-', '_') 423 pre = '' 424 if package_dirname[0].isdigit(): 425 pre = 'p' 426 return pre + package_dirname 427 428 429def sanitize_results_data(results_dirpath): 430 """Replace or remove any data that would possibly contain IP 431 432 Args: 433 results_dirpath: str; Path to job results directory 434 """ 435 raise NotImplementedError 436