Home | History | Annotate | Download | only in test
      1 """Base support for parser scenario testing.
      2 """
      3 
      4 from os import path
      5 import ConfigParser, os, shelve, shutil, sys, tarfile, time
      6 import difflib, itertools
      7 import common
      8 from autotest_lib.client.common_lib import utils, autotemp
      9 from autotest_lib.tko import parser_lib
     10 from autotest_lib.tko.parsers.test import templates
     11 from autotest_lib.tko.parsers.test import unittest_hotfix
     12 
     13 TEMPLATES_DIRPATH = templates.__path__[0]
     14 # Set TZ used to UTC
     15 os.environ['TZ'] = 'UTC'
     16 time.tzset()
     17 
     18 KEYVAL = 'keyval'
     19 STATUS_VERSION = 'status_version'
     20 PARSER_RESULT_STORE = 'parser_result.store'
     21 RESULTS_DIR_TARBALL = 'results_dir.tgz'
     22 CONFIG_FILENAME = 'scenario.cfg'
     23 TEST = 'test'
     24 PARSER_RESULT_TAG = 'parser_result_tag'
     25 
     26 
     27 class Error(Exception):
     28     pass
     29 
     30 
     31 class BadResultsDirectoryError(Error):
     32     pass
     33 
     34 
     35 class UnsupportedParserResultError(Error):
     36     pass
     37 
     38 
     39 class UnsupportedTemplateTypeError(Error):
     40     pass
     41 
     42 
     43 
     44 class 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 
     79 class 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 
    121 def 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 
    146 def 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 
    167 class 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 
    200 class 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 
    240 def 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 
    247 def 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 
    275 def 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 
    290 def 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 
    305 def 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 
    319 def 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 
    343 def 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 
    361 def 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 
    376 def 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 
    412 def 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 
    429 def 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