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