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