xref: /aosp_15_r20/external/autotest/tko/parsers/test/scenario_base.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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