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