| # Lint as: python2, python3 |
| """Base support for parser scenario testing. |
| """ |
| |
| from __future__ import absolute_import |
| from __future__ import division |
| from __future__ import print_function |
| from os import path |
| import six.moves.configparser, os, shelve, shutil, sys, tarfile, time |
| import difflib, itertools |
| import common |
| from autotest_lib.client.common_lib import utils, autotemp |
| from autotest_lib.tko import parser_lib |
| from autotest_lib.tko.parsers.test import templates |
| from autotest_lib.tko.parsers.test import unittest_hotfix |
| import six |
| from six.moves import zip |
| |
| TEMPLATES_DIRPATH = templates.__path__[0] |
| # Set TZ used to UTC |
| os.environ['TZ'] = 'UTC' |
| time.tzset() |
| |
| KEYVAL = 'keyval' |
| STATUS_VERSION = 'status_version' |
| PARSER_RESULT_STORE = 'parser_result.store' |
| RESULTS_DIR_TARBALL = 'results_dir.tgz' |
| CONFIG_FILENAME = 'scenario.cfg' |
| TEST = 'test' |
| PARSER_RESULT_TAG = 'parser_result_tag' |
| |
| |
| class Error(Exception): |
| pass |
| |
| |
| class BadResultsDirectoryError(Error): |
| pass |
| |
| |
| class UnsupportedParserResultError(Error): |
| pass |
| |
| |
| class UnsupportedTemplateTypeError(Error): |
| pass |
| |
| |
| |
| class ParserException(object): |
| """Abstract representation of exception raised from parser execution. |
| |
| We will want to persist exceptions raised from the parser but also change |
| the objects that make them up during refactor. For this reason |
| we can't merely pickle the original. |
| """ |
| |
| def __init__(self, orig): |
| """ |
| Args: |
| orig: Exception; To copy |
| """ |
| self.classname = orig.__class__.__name__ |
| print("Copying exception:", self.classname) |
| for key, val in six.iteritems(orig.__dict__): |
| setattr(self, key, val) |
| |
| |
| def __eq__(self, other): |
| """Test if equal to another ParserException.""" |
| return self.__dict__ == other.__dict__ |
| |
| |
| def __ne__(self, other): |
| """Test if not equal to another ParserException.""" |
| return self.__dict__ != other.__dict__ |
| |
| |
| def __str__(self): |
| sd = self.__dict__ |
| pairs = ['%s="%s"' % (k, sd[k]) for k in sorted(sd.keys())] |
| return "<%s: %s>" % (self.classname, ', '.join(pairs)) |
| |
| |
| class ParserTestResult(object): |
| """Abstract representation of test result parser state. |
| |
| We will want to persist test results but also change the |
| objects that make them up during refactor. For this reason |
| we can't merely pickle the originals. |
| """ |
| |
| def __init__(self, orig): |
| """ |
| Tracking all the attributes as they change over time is |
| not desirable. Instead we populate the instance's __dict__ |
| by introspecting orig. |
| |
| Args: |
| orig: testobj; Framework test result instance to copy. |
| """ |
| for key, val in six.iteritems(orig.__dict__): |
| if key == 'kernel': |
| setattr(self, key, dict(val.__dict__)) |
| elif key == 'iterations': |
| setattr(self, key, [dict(it.__dict__) for it in val]) |
| else: |
| setattr(self, key, val) |
| |
| |
| def __eq__(self, other): |
| """Test if equal to another ParserTestResult.""" |
| return self.__dict__ == other.__dict__ |
| |
| |
| def __ne__(self, other): |
| """Test if not equal to another ParserTestResult.""" |
| return self.__dict__ != other.__dict__ |
| |
| |
| def __str__(self): |
| sd = self.__dict__ |
| pairs = ['%s="%s"' % (k, sd[k]) for k in sorted(sd.keys())] |
| return "<%s: %s>" % (self.__class__.__name__, ', '.join(pairs)) |
| |
| |
| def copy_parser_result(parser_result): |
| """Copy parser_result into ParserTestResult instances. |
| |
| Args: |
| parser_result: |
| list; [testobj, ...] |
| - Or - |
| Exception |
| |
| Returns: |
| list; [ParserTestResult, ...] |
| - Or - |
| ParserException |
| |
| Raises: |
| UnsupportedParserResultError; If parser_result type is not supported |
| """ |
| if type(parser_result) is list: |
| return [ParserTestResult(test) for test in parser_result] |
| elif isinstance(parser_result, Exception): |
| return ParserException(parser_result) |
| else: |
| raise UnsupportedParserResultError |
| |
| |
| def compare_parser_results(left, right): |
| """Generates a textual report (for now) on the differences between. |
| |
| Args: |
| left: list of ParserTestResults or a single ParserException |
| right: list of ParserTestResults or a single ParserException |
| |
| Returns: Generator returned from difflib.Differ().compare() |
| """ |
| def to_los(obj): |
| """Generate a list of strings representation of object.""" |
| if type(obj) is list: |
| return [ |
| '%d) %s' % pair |
| for pair in zip(itertools.count(), obj)] |
| else: |
| return ['i) %s' % obj] |
| |
| return difflib.Differ().compare(to_los(left), to_los(right)) |
| |
| |
| class ParserHarness(object): |
| """Harness for objects related to the parser. |
| |
| This can exercise a parser on specific result data in various ways. |
| """ |
| |
| def __init__( |
| self, parser, job, job_keyval, status_version, status_log_filepath): |
| """ |
| Args: |
| parser: tko.parsers.base.parser; Subclass instance of base parser. |
| job: job implementation; Returned from parser.make_job() |
| job_keyval: dict; Result of parsing job keyval file. |
| status_version: str; Status log format version |
| status_log_filepath: str; Path to result data status.log file |
| """ |
| self.parser = parser |
| self.job = job |
| self.job_keyval = job_keyval |
| self.status_version = status_version |
| self.status_log_filepath = status_log_filepath |
| |
| |
| def execute(self): |
| """Basic exercise, pass entire log data into .end() |
| |
| Returns: list; [testobj, ...] |
| """ |
| status_lines = open(self.status_log_filepath).readlines() |
| self.parser.start(self.job) |
| return self.parser.end(status_lines) |
| |
| |
| class BaseScenarioTestCase(unittest_hotfix.TestCase): |
| """Base class for all Scenario TestCase implementations. |
| |
| This will load up all resources from scenario package directory upon |
| instantiation, and initialize a new ParserHarness before each test |
| method execution. |
| """ |
| def __init__(self, methodName='runTest'): |
| unittest_hotfix.TestCase.__init__(self, methodName) |
| self.package_dirpath = path.dirname( |
| sys.modules[self.__module__].__file__) |
| self.tmp_dirpath, self.results_dirpath = load_results_dir( |
| self.package_dirpath) |
| self.parser_result_store = load_parser_result_store( |
| self.package_dirpath) |
| self.config = load_config(self.package_dirpath) |
| self.parser_result_tag = self.config.get( |
| TEST, PARSER_RESULT_TAG) |
| self.expected_status_version = self.config.getint( |
| TEST, STATUS_VERSION) |
| self.harness = None |
| |
| |
| def setUp(self): |
| if self.results_dirpath: |
| self.harness = new_parser_harness(self.results_dirpath) |
| |
| |
| def tearDown(self): |
| if self.tmp_dirpath: |
| self.tmp_dirpath.clean() |
| |
| |
| def test_status_version(self): |
| """Ensure basic functionality.""" |
| self.skipIf(not self.harness) |
| self.assertEquals( |
| self.harness.status_version, self.expected_status_version) |
| |
| |
| def shelve_open(filename, flag='c', protocol=None, writeback=False): |
| """A more system-portable wrapper around shelve.open, with the exact |
| same arguments and interpretation.""" |
| import dumbdbm |
| return shelve.Shelf(dumbdbm.open(filename, flag), protocol, writeback) |
| |
| |
| def new_parser_harness(results_dirpath): |
| """Ensure valid environment and create new parser with wrapper. |
| |
| Args: |
| results_dirpath: str; Path to job results directory |
| |
| Returns: |
| ParserHarness; |
| |
| Raises: |
| BadResultsDirectoryError; If results dir does not exist or is malformed. |
| """ |
| if not path.exists(results_dirpath): |
| raise BadResultsDirectoryError |
| |
| keyval_path = path.join(results_dirpath, KEYVAL) |
| job_keyval = utils.read_keyval(keyval_path) |
| status_version = job_keyval[STATUS_VERSION] |
| parser = parser_lib.parser(status_version) |
| job = parser.make_job(results_dirpath) |
| status_log_filepath = path.join(results_dirpath, 'status.log') |
| if not path.exists(status_log_filepath): |
| raise BadResultsDirectoryError |
| |
| return ParserHarness( |
| parser, job, job_keyval, status_version, status_log_filepath) |
| |
| |
| def store_parser_result(package_dirpath, parser_result, tag): |
| """Persist parser result to specified scenario package, keyed by tag. |
| |
| Args: |
| package_dirpath: str; Path to scenario package directory. |
| parser_result: list or Exception; Result from ParserHarness.execute |
| tag: str; Tag to use as shelve key for persisted parser_result |
| """ |
| copy = copy_parser_result(parser_result) |
| sto_filepath = path.join(package_dirpath, PARSER_RESULT_STORE) |
| sto = shelve_open(sto_filepath) |
| sto[tag] = copy |
| sto.close() |
| |
| |
| def load_parser_result_store(package_dirpath, open_for_write=False): |
| """Load parser result store from specified scenario package. |
| |
| Args: |
| package_dirpath: str; Path to scenario package directory. |
| open_for_write: bool; Open store for writing. |
| |
| Returns: |
| shelve.DbfilenameShelf; Looks and acts like a dict |
| """ |
| open_flag = open_for_write and 'c' or 'r' |
| sto_filepath = path.join(package_dirpath, PARSER_RESULT_STORE) |
| return shelve_open(sto_filepath, flag=open_flag) |
| |
| |
| def store_results_dir(package_dirpath, results_dirpath): |
| """Make tarball of results_dirpath in package_dirpath. |
| |
| Args: |
| package_dirpath: str; Path to scenario package directory. |
| results_dirpath: str; Path to job results directory |
| """ |
| tgz_filepath = path.join(package_dirpath, RESULTS_DIR_TARBALL) |
| tgz = tarfile.open(tgz_filepath, 'w:gz') |
| results_dirname = path.basename(results_dirpath) |
| tgz.add(results_dirpath, results_dirname) |
| tgz.close() |
| |
| |
| def load_results_dir(package_dirpath): |
| """Unpack results tarball in package_dirpath to temp dir. |
| |
| Args: |
| package_dirpath: str; Path to scenario package directory. |
| |
| Returns: |
| str; New temp path for extracted results directory. |
| - Or - |
| None; If tarball does not exist |
| """ |
| tgz_filepath = path.join(package_dirpath, RESULTS_DIR_TARBALL) |
| if not path.exists(tgz_filepath): |
| return None, None |
| |
| tgz = tarfile.open(tgz_filepath, 'r:gz') |
| tmp_dirpath = autotemp.tempdir(unique_id='scenario_base') |
| results_dirname = tgz.next().name |
| tgz.extract(results_dirname, tmp_dirpath.name) |
| for info in tgz: |
| tgz.extract(info.name, tmp_dirpath.name) |
| return tmp_dirpath, path.join(tmp_dirpath.name, results_dirname) |
| |
| |
| def write_config(package_dirpath, **properties): |
| """Write test configuration file to package_dirpath. |
| |
| Args: |
| package_dirpath: str; Path to scenario package directory. |
| properties: dict; Key value entries to write to to config file. |
| """ |
| config = six.moves.configparser.RawConfigParser() |
| config.add_section(TEST) |
| for key, val in six.iteritems(properties): |
| config.set(TEST, key, val) |
| |
| config_filepath = path.join(package_dirpath, CONFIG_FILENAME) |
| fi = open(config_filepath, 'w') |
| config.write(fi) |
| fi.close() |
| |
| |
| def load_config(package_dirpath): |
| """Load config from package_dirpath. |
| |
| Args: |
| package_dirpath: str; Path to scenario package directory. |
| |
| Returns: |
| ConfigParser.RawConfigParser; |
| """ |
| config = six.moves.configparser.RawConfigParser() |
| config_filepath = path.join(package_dirpath, CONFIG_FILENAME) |
| config.read(config_filepath) |
| return config |
| |
| |
| def install_unittest_module(package_dirpath, template_type): |
| """Install specified unittest template module to package_dirpath. |
| |
| Template modules are stored in tko/parsers/test/templates. |
| Installation includes: |
| Copying to package_dirpath/template_type_unittest.py |
| Copying scenario package common.py to package_dirpath |
| Touching package_dirpath/__init__.py |
| |
| Args: |
| package_dirpath: str; Path to scenario package directory. |
| template_type: str; Name of template module to install. |
| |
| Raises: |
| UnsupportedTemplateTypeError; If there is no module in |
| templates package called template_type. |
| """ |
| from_filepath = path.join( |
| TEMPLATES_DIRPATH, '%s.py' % template_type) |
| if not path.exists(from_filepath): |
| raise UnsupportedTemplateTypeError |
| |
| to_filepath = path.join( |
| package_dirpath, '%s_unittest.py' % template_type) |
| shutil.copy(from_filepath, to_filepath) |
| |
| # For convenience we must copy the common.py hack file too :-( |
| from_common_filepath = path.join( |
| TEMPLATES_DIRPATH, 'scenario_package_common.py') |
| to_common_filepath = path.join(package_dirpath, 'common.py') |
| shutil.copy(from_common_filepath, to_common_filepath) |
| |
| # And last but not least, touch an __init__ file |
| os.mknod(path.join(package_dirpath, '__init__.py')) |
| |
| |
| def fix_package_dirname(package_dirname): |
| """Convert package_dirname to a valid package name string, if necessary. |
| |
| Args: |
| package_dirname: str; Name of scenario package directory. |
| |
| Returns: |
| str; Possibly fixed package_dirname |
| """ |
| # Really stupid atm, just enough to handle results dirnames |
| package_dirname = package_dirname.replace('-', '_') |
| pre = '' |
| if package_dirname[0].isdigit(): |
| pre = 'p' |
| return pre + package_dirname |
| |
| |
| def sanitize_results_data(results_dirpath): |
| """Replace or remove any data that would possibly contain IP |
| |
| Args: |
| results_dirpath: str; Path to job results directory |
| """ |
| raise NotImplementedError |