xref: /aosp_15_r20/external/autotest/tko/parsers/test/scenario_base.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li"""Base support for parser scenario testing.
3*9c5db199SXin Li"""
4*9c5db199SXin Li
5*9c5db199SXin Lifrom __future__ import absolute_import
6*9c5db199SXin Lifrom __future__ import division
7*9c5db199SXin Lifrom __future__ import print_function
8*9c5db199SXin Lifrom os import path
9*9c5db199SXin Liimport six.moves.configparser, os, shelve, shutil, sys, tarfile, time
10*9c5db199SXin Liimport difflib, itertools
11*9c5db199SXin Liimport common
12*9c5db199SXin Lifrom autotest_lib.client.common_lib import utils, autotemp
13*9c5db199SXin Lifrom autotest_lib.tko import parser_lib
14*9c5db199SXin Lifrom autotest_lib.tko.parsers.test import templates
15*9c5db199SXin Lifrom autotest_lib.tko.parsers.test import unittest_hotfix
16*9c5db199SXin Liimport six
17*9c5db199SXin Lifrom six.moves import zip
18*9c5db199SXin Li
19*9c5db199SXin LiTEMPLATES_DIRPATH = templates.__path__[0]
20*9c5db199SXin Li# Set TZ used to UTC
21*9c5db199SXin Lios.environ['TZ'] = 'UTC'
22*9c5db199SXin Litime.tzset()
23*9c5db199SXin Li
24*9c5db199SXin LiKEYVAL = 'keyval'
25*9c5db199SXin LiSTATUS_VERSION = 'status_version'
26*9c5db199SXin LiPARSER_RESULT_STORE = 'parser_result.store'
27*9c5db199SXin LiRESULTS_DIR_TARBALL = 'results_dir.tgz'
28*9c5db199SXin LiCONFIG_FILENAME = 'scenario.cfg'
29*9c5db199SXin LiTEST = 'test'
30*9c5db199SXin LiPARSER_RESULT_TAG = 'parser_result_tag'
31*9c5db199SXin Li
32*9c5db199SXin Li
33*9c5db199SXin Liclass Error(Exception):
34*9c5db199SXin Li    pass
35*9c5db199SXin Li
36*9c5db199SXin Li
37*9c5db199SXin Liclass BadResultsDirectoryError(Error):
38*9c5db199SXin Li    pass
39*9c5db199SXin Li
40*9c5db199SXin Li
41*9c5db199SXin Liclass UnsupportedParserResultError(Error):
42*9c5db199SXin Li    pass
43*9c5db199SXin Li
44*9c5db199SXin Li
45*9c5db199SXin Liclass UnsupportedTemplateTypeError(Error):
46*9c5db199SXin Li    pass
47*9c5db199SXin Li
48*9c5db199SXin Li
49*9c5db199SXin Li
50*9c5db199SXin Liclass ParserException(object):
51*9c5db199SXin Li    """Abstract representation of exception raised from parser execution.
52*9c5db199SXin Li
53*9c5db199SXin Li    We will want to persist exceptions raised from the parser but also change
54*9c5db199SXin Li    the objects that make them up during refactor. For this reason
55*9c5db199SXin Li    we can't merely pickle the original.
56*9c5db199SXin Li    """
57*9c5db199SXin Li
58*9c5db199SXin Li    def __init__(self, orig):
59*9c5db199SXin Li        """
60*9c5db199SXin Li        Args:
61*9c5db199SXin Li          orig: Exception; To copy
62*9c5db199SXin Li        """
63*9c5db199SXin Li        self.classname = orig.__class__.__name__
64*9c5db199SXin Li        print("Copying exception:", self.classname)
65*9c5db199SXin Li        for key, val in six.iteritems(orig.__dict__):
66*9c5db199SXin Li            setattr(self, key, val)
67*9c5db199SXin Li
68*9c5db199SXin Li
69*9c5db199SXin Li    def __eq__(self, other):
70*9c5db199SXin Li        """Test if equal to another ParserException."""
71*9c5db199SXin Li        return self.__dict__ == other.__dict__
72*9c5db199SXin Li
73*9c5db199SXin Li
74*9c5db199SXin Li    def __ne__(self, other):
75*9c5db199SXin Li        """Test if not equal to another ParserException."""
76*9c5db199SXin Li        return self.__dict__ != other.__dict__
77*9c5db199SXin Li
78*9c5db199SXin Li
79*9c5db199SXin Li    def __str__(self):
80*9c5db199SXin Li        sd = self.__dict__
81*9c5db199SXin Li        pairs = ['%s="%s"' % (k, sd[k]) for k in sorted(sd.keys())]
82*9c5db199SXin Li        return "<%s: %s>" % (self.classname, ', '.join(pairs))
83*9c5db199SXin Li
84*9c5db199SXin Li
85*9c5db199SXin Liclass ParserTestResult(object):
86*9c5db199SXin Li    """Abstract representation of test result parser state.
87*9c5db199SXin Li
88*9c5db199SXin Li    We will want to persist test results but also change the
89*9c5db199SXin Li    objects that make them up during refactor. For this reason
90*9c5db199SXin Li    we can't merely pickle the originals.
91*9c5db199SXin Li    """
92*9c5db199SXin Li
93*9c5db199SXin Li    def __init__(self, orig):
94*9c5db199SXin Li        """
95*9c5db199SXin Li        Tracking all the attributes as they change over time is
96*9c5db199SXin Li        not desirable. Instead we populate the instance's __dict__
97*9c5db199SXin Li        by introspecting orig.
98*9c5db199SXin Li
99*9c5db199SXin Li        Args:
100*9c5db199SXin Li            orig: testobj; Framework test result instance to copy.
101*9c5db199SXin Li        """
102*9c5db199SXin Li        for key, val in six.iteritems(orig.__dict__):
103*9c5db199SXin Li            if key == 'kernel':
104*9c5db199SXin Li                setattr(self, key, dict(val.__dict__))
105*9c5db199SXin Li            elif key == 'iterations':
106*9c5db199SXin Li                setattr(self, key, [dict(it.__dict__) for it in val])
107*9c5db199SXin Li            else:
108*9c5db199SXin Li                setattr(self, key, val)
109*9c5db199SXin Li
110*9c5db199SXin Li
111*9c5db199SXin Li    def __eq__(self, other):
112*9c5db199SXin Li        """Test if equal to another ParserTestResult."""
113*9c5db199SXin Li        return self.__dict__ == other.__dict__
114*9c5db199SXin Li
115*9c5db199SXin Li
116*9c5db199SXin Li    def __ne__(self, other):
117*9c5db199SXin Li        """Test if not equal to another ParserTestResult."""
118*9c5db199SXin Li        return self.__dict__ != other.__dict__
119*9c5db199SXin Li
120*9c5db199SXin Li
121*9c5db199SXin Li    def __str__(self):
122*9c5db199SXin Li        sd = self.__dict__
123*9c5db199SXin Li        pairs = ['%s="%s"' % (k, sd[k]) for k in sorted(sd.keys())]
124*9c5db199SXin Li        return "<%s: %s>" % (self.__class__.__name__, ', '.join(pairs))
125*9c5db199SXin Li
126*9c5db199SXin Li
127*9c5db199SXin Lidef copy_parser_result(parser_result):
128*9c5db199SXin Li    """Copy parser_result into ParserTestResult instances.
129*9c5db199SXin Li
130*9c5db199SXin Li    Args:
131*9c5db199SXin Li      parser_result:
132*9c5db199SXin Li          list; [testobj, ...]
133*9c5db199SXin Li          - Or -
134*9c5db199SXin Li          Exception
135*9c5db199SXin Li
136*9c5db199SXin Li    Returns:
137*9c5db199SXin Li      list; [ParserTestResult, ...]
138*9c5db199SXin Li      - Or -
139*9c5db199SXin Li      ParserException
140*9c5db199SXin Li
141*9c5db199SXin Li    Raises:
142*9c5db199SXin Li        UnsupportedParserResultError; If parser_result type is not supported
143*9c5db199SXin Li    """
144*9c5db199SXin Li    if type(parser_result) is list:
145*9c5db199SXin Li        return [ParserTestResult(test) for test in parser_result]
146*9c5db199SXin Li    elif isinstance(parser_result, Exception):
147*9c5db199SXin Li        return ParserException(parser_result)
148*9c5db199SXin Li    else:
149*9c5db199SXin Li        raise UnsupportedParserResultError
150*9c5db199SXin Li
151*9c5db199SXin Li
152*9c5db199SXin Lidef compare_parser_results(left, right):
153*9c5db199SXin Li    """Generates a textual report (for now) on the differences between.
154*9c5db199SXin Li
155*9c5db199SXin Li    Args:
156*9c5db199SXin Li      left: list of ParserTestResults or a single ParserException
157*9c5db199SXin Li      right: list of ParserTestResults or a single ParserException
158*9c5db199SXin Li
159*9c5db199SXin Li    Returns: Generator returned from difflib.Differ().compare()
160*9c5db199SXin Li    """
161*9c5db199SXin Li    def to_los(obj):
162*9c5db199SXin Li        """Generate a list of strings representation of object."""
163*9c5db199SXin Li        if type(obj) is list:
164*9c5db199SXin Li            return [
165*9c5db199SXin Li                '%d) %s' % pair
166*9c5db199SXin Li                for pair in zip(itertools.count(), obj)]
167*9c5db199SXin Li        else:
168*9c5db199SXin Li            return ['i) %s' % obj]
169*9c5db199SXin Li
170*9c5db199SXin Li    return difflib.Differ().compare(to_los(left), to_los(right))
171*9c5db199SXin Li
172*9c5db199SXin Li
173*9c5db199SXin Liclass ParserHarness(object):
174*9c5db199SXin Li    """Harness for objects related to the parser.
175*9c5db199SXin Li
176*9c5db199SXin Li    This can exercise a parser on specific result data in various ways.
177*9c5db199SXin Li    """
178*9c5db199SXin Li
179*9c5db199SXin Li    def __init__(
180*9c5db199SXin Li        self, parser, job, job_keyval, status_version, status_log_filepath):
181*9c5db199SXin Li        """
182*9c5db199SXin Li        Args:
183*9c5db199SXin Li          parser: tko.parsers.base.parser; Subclass instance of base parser.
184*9c5db199SXin Li          job: job implementation; Returned from parser.make_job()
185*9c5db199SXin Li          job_keyval: dict; Result of parsing job keyval file.
186*9c5db199SXin Li          status_version: str; Status log format version
187*9c5db199SXin Li          status_log_filepath: str; Path to result data status.log file
188*9c5db199SXin Li        """
189*9c5db199SXin Li        self.parser = parser
190*9c5db199SXin Li        self.job = job
191*9c5db199SXin Li        self.job_keyval = job_keyval
192*9c5db199SXin Li        self.status_version = status_version
193*9c5db199SXin Li        self.status_log_filepath = status_log_filepath
194*9c5db199SXin Li
195*9c5db199SXin Li
196*9c5db199SXin Li    def execute(self):
197*9c5db199SXin Li        """Basic exercise, pass entire log data into .end()
198*9c5db199SXin Li
199*9c5db199SXin Li        Returns: list; [testobj, ...]
200*9c5db199SXin Li        """
201*9c5db199SXin Li        status_lines = open(self.status_log_filepath).readlines()
202*9c5db199SXin Li        self.parser.start(self.job)
203*9c5db199SXin Li        return self.parser.end(status_lines)
204*9c5db199SXin Li
205*9c5db199SXin Li
206*9c5db199SXin Liclass BaseScenarioTestCase(unittest_hotfix.TestCase):
207*9c5db199SXin Li    """Base class for all Scenario TestCase implementations.
208*9c5db199SXin Li
209*9c5db199SXin Li    This will load up all resources from scenario package directory upon
210*9c5db199SXin Li    instantiation, and initialize a new ParserHarness before each test
211*9c5db199SXin Li    method execution.
212*9c5db199SXin Li    """
213*9c5db199SXin Li    def __init__(self, methodName='runTest'):
214*9c5db199SXin Li        unittest_hotfix.TestCase.__init__(self, methodName)
215*9c5db199SXin Li        self.package_dirpath = path.dirname(
216*9c5db199SXin Li            sys.modules[self.__module__].__file__)
217*9c5db199SXin Li        self.tmp_dirpath, self.results_dirpath = load_results_dir(
218*9c5db199SXin Li            self.package_dirpath)
219*9c5db199SXin Li        self.parser_result_store = load_parser_result_store(
220*9c5db199SXin Li            self.package_dirpath)
221*9c5db199SXin Li        self.config = load_config(self.package_dirpath)
222*9c5db199SXin Li        self.parser_result_tag = self.config.get(
223*9c5db199SXin Li            TEST, PARSER_RESULT_TAG)
224*9c5db199SXin Li        self.expected_status_version = self.config.getint(
225*9c5db199SXin Li            TEST, STATUS_VERSION)
226*9c5db199SXin Li        self.harness = None
227*9c5db199SXin Li
228*9c5db199SXin Li
229*9c5db199SXin Li    def setUp(self):
230*9c5db199SXin Li        if self.results_dirpath:
231*9c5db199SXin Li            self.harness = new_parser_harness(self.results_dirpath)
232*9c5db199SXin Li
233*9c5db199SXin Li
234*9c5db199SXin Li    def tearDown(self):
235*9c5db199SXin Li        if self.tmp_dirpath:
236*9c5db199SXin Li            self.tmp_dirpath.clean()
237*9c5db199SXin Li
238*9c5db199SXin Li
239*9c5db199SXin Li    def test_status_version(self):
240*9c5db199SXin Li        """Ensure basic functionality."""
241*9c5db199SXin Li        self.skipIf(not self.harness)
242*9c5db199SXin Li        self.assertEquals(
243*9c5db199SXin Li            self.harness.status_version, self.expected_status_version)
244*9c5db199SXin Li
245*9c5db199SXin Li
246*9c5db199SXin Lidef shelve_open(filename, flag='c', protocol=None, writeback=False):
247*9c5db199SXin Li    """A more system-portable wrapper around shelve.open, with the exact
248*9c5db199SXin Li    same arguments and interpretation."""
249*9c5db199SXin Li    import dumbdbm
250*9c5db199SXin Li    return shelve.Shelf(dumbdbm.open(filename, flag), protocol, writeback)
251*9c5db199SXin Li
252*9c5db199SXin Li
253*9c5db199SXin Lidef new_parser_harness(results_dirpath):
254*9c5db199SXin Li    """Ensure valid environment and create new parser with wrapper.
255*9c5db199SXin Li
256*9c5db199SXin Li    Args:
257*9c5db199SXin Li      results_dirpath: str; Path to job results directory
258*9c5db199SXin Li
259*9c5db199SXin Li    Returns:
260*9c5db199SXin Li      ParserHarness;
261*9c5db199SXin Li
262*9c5db199SXin Li    Raises:
263*9c5db199SXin Li      BadResultsDirectoryError; If results dir does not exist or is malformed.
264*9c5db199SXin Li    """
265*9c5db199SXin Li    if not path.exists(results_dirpath):
266*9c5db199SXin Li        raise BadResultsDirectoryError
267*9c5db199SXin Li
268*9c5db199SXin Li    keyval_path = path.join(results_dirpath, KEYVAL)
269*9c5db199SXin Li    job_keyval = utils.read_keyval(keyval_path)
270*9c5db199SXin Li    status_version = job_keyval[STATUS_VERSION]
271*9c5db199SXin Li    parser = parser_lib.parser(status_version)
272*9c5db199SXin Li    job = parser.make_job(results_dirpath)
273*9c5db199SXin Li    status_log_filepath = path.join(results_dirpath, 'status.log')
274*9c5db199SXin Li    if not path.exists(status_log_filepath):
275*9c5db199SXin Li        raise BadResultsDirectoryError
276*9c5db199SXin Li
277*9c5db199SXin Li    return ParserHarness(
278*9c5db199SXin Li        parser, job, job_keyval, status_version, status_log_filepath)
279*9c5db199SXin Li
280*9c5db199SXin Li
281*9c5db199SXin Lidef store_parser_result(package_dirpath, parser_result, tag):
282*9c5db199SXin Li    """Persist parser result to specified scenario package, keyed by tag.
283*9c5db199SXin Li
284*9c5db199SXin Li    Args:
285*9c5db199SXin Li      package_dirpath: str; Path to scenario package directory.
286*9c5db199SXin Li      parser_result: list or Exception; Result from ParserHarness.execute
287*9c5db199SXin Li      tag: str; Tag to use as shelve key for persisted parser_result
288*9c5db199SXin Li    """
289*9c5db199SXin Li    copy = copy_parser_result(parser_result)
290*9c5db199SXin Li    sto_filepath = path.join(package_dirpath, PARSER_RESULT_STORE)
291*9c5db199SXin Li    sto = shelve_open(sto_filepath)
292*9c5db199SXin Li    sto[tag] = copy
293*9c5db199SXin Li    sto.close()
294*9c5db199SXin Li
295*9c5db199SXin Li
296*9c5db199SXin Lidef load_parser_result_store(package_dirpath, open_for_write=False):
297*9c5db199SXin Li    """Load parser result store from specified scenario package.
298*9c5db199SXin Li
299*9c5db199SXin Li    Args:
300*9c5db199SXin Li      package_dirpath: str; Path to scenario package directory.
301*9c5db199SXin Li      open_for_write: bool; Open store for writing.
302*9c5db199SXin Li
303*9c5db199SXin Li    Returns:
304*9c5db199SXin Li      shelve.DbfilenameShelf; Looks and acts like a dict
305*9c5db199SXin Li    """
306*9c5db199SXin Li    open_flag = open_for_write and 'c' or 'r'
307*9c5db199SXin Li    sto_filepath = path.join(package_dirpath, PARSER_RESULT_STORE)
308*9c5db199SXin Li    return shelve_open(sto_filepath, flag=open_flag)
309*9c5db199SXin Li
310*9c5db199SXin Li
311*9c5db199SXin Lidef store_results_dir(package_dirpath, results_dirpath):
312*9c5db199SXin Li    """Make tarball of results_dirpath in package_dirpath.
313*9c5db199SXin Li
314*9c5db199SXin Li    Args:
315*9c5db199SXin Li      package_dirpath: str; Path to scenario package directory.
316*9c5db199SXin Li      results_dirpath: str; Path to job results directory
317*9c5db199SXin Li    """
318*9c5db199SXin Li    tgz_filepath = path.join(package_dirpath, RESULTS_DIR_TARBALL)
319*9c5db199SXin Li    tgz = tarfile.open(tgz_filepath, 'w:gz')
320*9c5db199SXin Li    results_dirname = path.basename(results_dirpath)
321*9c5db199SXin Li    tgz.add(results_dirpath, results_dirname)
322*9c5db199SXin Li    tgz.close()
323*9c5db199SXin Li
324*9c5db199SXin Li
325*9c5db199SXin Lidef load_results_dir(package_dirpath):
326*9c5db199SXin Li    """Unpack results tarball in package_dirpath to temp dir.
327*9c5db199SXin Li
328*9c5db199SXin Li    Args:
329*9c5db199SXin Li      package_dirpath: str; Path to scenario package directory.
330*9c5db199SXin Li
331*9c5db199SXin Li    Returns:
332*9c5db199SXin Li      str; New temp path for extracted results directory.
333*9c5db199SXin Li      - Or -
334*9c5db199SXin Li      None; If tarball does not exist
335*9c5db199SXin Li    """
336*9c5db199SXin Li    tgz_filepath = path.join(package_dirpath, RESULTS_DIR_TARBALL)
337*9c5db199SXin Li    if not path.exists(tgz_filepath):
338*9c5db199SXin Li        return None, None
339*9c5db199SXin Li
340*9c5db199SXin Li    tgz = tarfile.open(tgz_filepath, 'r:gz')
341*9c5db199SXin Li    tmp_dirpath = autotemp.tempdir(unique_id='scenario_base')
342*9c5db199SXin Li    results_dirname = tgz.next().name
343*9c5db199SXin Li    tgz.extract(results_dirname, tmp_dirpath.name)
344*9c5db199SXin Li    for info in tgz:
345*9c5db199SXin Li        tgz.extract(info.name, tmp_dirpath.name)
346*9c5db199SXin Li    return tmp_dirpath, path.join(tmp_dirpath.name, results_dirname)
347*9c5db199SXin Li
348*9c5db199SXin Li
349*9c5db199SXin Lidef write_config(package_dirpath, **properties):
350*9c5db199SXin Li    """Write test configuration file to package_dirpath.
351*9c5db199SXin Li
352*9c5db199SXin Li    Args:
353*9c5db199SXin Li      package_dirpath: str; Path to scenario package directory.
354*9c5db199SXin Li      properties: dict; Key value entries to write to to config file.
355*9c5db199SXin Li    """
356*9c5db199SXin Li    config = six.moves.configparser.RawConfigParser()
357*9c5db199SXin Li    config.add_section(TEST)
358*9c5db199SXin Li    for key, val in six.iteritems(properties):
359*9c5db199SXin Li        config.set(TEST, key, val)
360*9c5db199SXin Li
361*9c5db199SXin Li    config_filepath = path.join(package_dirpath, CONFIG_FILENAME)
362*9c5db199SXin Li    fi = open(config_filepath, 'w')
363*9c5db199SXin Li    config.write(fi)
364*9c5db199SXin Li    fi.close()
365*9c5db199SXin Li
366*9c5db199SXin Li
367*9c5db199SXin Lidef load_config(package_dirpath):
368*9c5db199SXin Li    """Load config from package_dirpath.
369*9c5db199SXin Li
370*9c5db199SXin Li    Args:
371*9c5db199SXin Li      package_dirpath: str; Path to scenario package directory.
372*9c5db199SXin Li
373*9c5db199SXin Li    Returns:
374*9c5db199SXin Li      ConfigParser.RawConfigParser;
375*9c5db199SXin Li    """
376*9c5db199SXin Li    config = six.moves.configparser.RawConfigParser()
377*9c5db199SXin Li    config_filepath = path.join(package_dirpath, CONFIG_FILENAME)
378*9c5db199SXin Li    config.read(config_filepath)
379*9c5db199SXin Li    return config
380*9c5db199SXin Li
381*9c5db199SXin Li
382*9c5db199SXin Lidef install_unittest_module(package_dirpath, template_type):
383*9c5db199SXin Li    """Install specified unittest template module to package_dirpath.
384*9c5db199SXin Li
385*9c5db199SXin Li    Template modules are stored in tko/parsers/test/templates.
386*9c5db199SXin Li    Installation includes:
387*9c5db199SXin Li      Copying to package_dirpath/template_type_unittest.py
388*9c5db199SXin Li      Copying scenario package common.py to package_dirpath
389*9c5db199SXin Li      Touching package_dirpath/__init__.py
390*9c5db199SXin Li
391*9c5db199SXin Li    Args:
392*9c5db199SXin Li      package_dirpath: str; Path to scenario package directory.
393*9c5db199SXin Li      template_type: str; Name of template module to install.
394*9c5db199SXin Li
395*9c5db199SXin Li    Raises:
396*9c5db199SXin Li      UnsupportedTemplateTypeError; If there is no module in
397*9c5db199SXin Li          templates package called template_type.
398*9c5db199SXin Li    """
399*9c5db199SXin Li    from_filepath = path.join(
400*9c5db199SXin Li        TEMPLATES_DIRPATH, '%s.py' % template_type)
401*9c5db199SXin Li    if not path.exists(from_filepath):
402*9c5db199SXin Li        raise UnsupportedTemplateTypeError
403*9c5db199SXin Li
404*9c5db199SXin Li    to_filepath = path.join(
405*9c5db199SXin Li        package_dirpath, '%s_unittest.py' % template_type)
406*9c5db199SXin Li    shutil.copy(from_filepath, to_filepath)
407*9c5db199SXin Li
408*9c5db199SXin Li    # For convenience we must copy the common.py hack file too :-(
409*9c5db199SXin Li    from_common_filepath = path.join(
410*9c5db199SXin Li        TEMPLATES_DIRPATH, 'scenario_package_common.py')
411*9c5db199SXin Li    to_common_filepath = path.join(package_dirpath, 'common.py')
412*9c5db199SXin Li    shutil.copy(from_common_filepath, to_common_filepath)
413*9c5db199SXin Li
414*9c5db199SXin Li    # And last but not least, touch an __init__ file
415*9c5db199SXin Li    os.mknod(path.join(package_dirpath, '__init__.py'))
416*9c5db199SXin Li
417*9c5db199SXin Li
418*9c5db199SXin Lidef fix_package_dirname(package_dirname):
419*9c5db199SXin Li    """Convert package_dirname to a valid package name string, if necessary.
420*9c5db199SXin Li
421*9c5db199SXin Li    Args:
422*9c5db199SXin Li      package_dirname: str; Name of scenario package directory.
423*9c5db199SXin Li
424*9c5db199SXin Li    Returns:
425*9c5db199SXin Li      str; Possibly fixed package_dirname
426*9c5db199SXin Li    """
427*9c5db199SXin Li    # Really stupid atm, just enough to handle results dirnames
428*9c5db199SXin Li    package_dirname = package_dirname.replace('-', '_')
429*9c5db199SXin Li    pre = ''
430*9c5db199SXin Li    if package_dirname[0].isdigit():
431*9c5db199SXin Li        pre = 'p'
432*9c5db199SXin Li    return pre + package_dirname
433*9c5db199SXin Li
434*9c5db199SXin Li
435*9c5db199SXin Lidef sanitize_results_data(results_dirpath):
436*9c5db199SXin Li    """Replace or remove any data that would possibly contain IP
437*9c5db199SXin Li
438*9c5db199SXin Li    Args:
439*9c5db199SXin Li      results_dirpath: str; Path to job results directory
440*9c5db199SXin Li    """
441*9c5db199SXin Li    raise NotImplementedError
442