You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
436 lines
13 KiB
436 lines
13 KiB
"""Base support for parser scenario testing.
|
|
"""
|
|
|
|
from os import path
|
|
import 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
|
|
|
|
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 orig.__dict__.iteritems():
|
|
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 orig.__dict__.iteritems():
|
|
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 itertools.izip(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 sanity."""
|
|
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 sane 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 = ConfigParser.RawConfigParser()
|
|
config.add_section(TEST)
|
|
for key, val in properties.iteritems():
|
|
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 = 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
|