Home | History | Annotate | Download | only in host
      1 #
      2 # Copyright (C) 2016 The Android Open Source Project
      3 #
      4 # Licensed under the Apache License, Version 2.0 (the "License");
      5 # you may not use this file except in compliance with the License.
      6 # You may obtain a copy of the License at
      7 #
      8 #      http://www.apache.org/licenses/LICENSE-2.0
      9 #
     10 # Unless required by applicable law or agreed to in writing, software
     11 # distributed under the License is distributed on an "AS IS" BASIS,
     12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13 # See the License for the specific language governing permissions and
     14 # limitations under the License.
     15 #
     16 """This module is where all the record definitions and record containers live.
     17 """
     18 
     19 import json
     20 import logging
     21 import pprint
     22 
     23 from vts.runners.host import signals
     24 from vts.runners.host import utils
     25 from vts.utils.python.common import list_utils
     26 
     27 
     28 class TestResultEnums(object):
     29     """Enums used for TestResultRecord class.
     30 
     31     Includes the tokens to mark test result with, and the string names for each
     32     field in TestResultRecord.
     33     """
     34 
     35     RECORD_NAME = "Test Name"
     36     RECORD_CLASS = "Test Class"
     37     RECORD_BEGIN_TIME = "Begin Time"
     38     RECORD_END_TIME = "End Time"
     39     RECORD_RESULT = "Result"
     40     RECORD_UID = "UID"
     41     RECORD_EXTRAS = "Extras"
     42     RECORD_EXTRA_ERRORS = "Extra Errors"
     43     RECORD_DETAILS = "Details"
     44     RECORD_TABLES = "Tables"
     45     TEST_RESULT_PASS = "PASS"
     46     TEST_RESULT_FAIL = "FAIL"
     47     TEST_RESULT_SKIP = "SKIP"
     48     TEST_RESULT_ERROR = "ERROR"
     49 
     50 
     51 class TestResultRecord(object):
     52     """A record that holds the information of a test case execution.
     53 
     54     Attributes:
     55         test_name: A string representing the name of the test case.
     56         begin_time: Epoch timestamp of when the test case started.
     57         end_time: Epoch timestamp of when the test case ended.
     58         uid: Unique identifier of a test case.
     59         result: Test result, PASS/FAIL/SKIP.
     60         extras: User defined extra information of the test result.
     61         details: A string explaining the details of the test case.
     62         tables: A dict of 2-dimensional lists containing tabular results.
     63     """
     64 
     65     def __init__(self, t_name, t_class=None):
     66         self.test_name = t_name
     67         self.test_class = t_class
     68         self.begin_time = None
     69         self.end_time = None
     70         self.uid = None
     71         self.result = None
     72         self.extras = None
     73         self.details = None
     74         self.extra_errors = {}
     75         self.tables = {}
     76 
     77     def testBegin(self):
     78         """Call this when the test case it records begins execution.
     79 
     80         Sets the begin_time of this record.
     81         """
     82         self.begin_time = utils.get_current_epoch_time()
     83 
     84     def _testEnd(self, result, e):
     85         """Class internal function to signal the end of a test case execution.
     86 
     87         Args:
     88             result: One of the TEST_RESULT enums in TestResultEnums.
     89             e: A test termination signal (usually an exception object). It can
     90                 be any exception instance or of any subclass of
     91                 vts.runners.host.signals.TestSignal.
     92         """
     93         self.end_time = utils.get_current_epoch_time()
     94         self.result = result
     95         if isinstance(e, signals.TestSignal):
     96             self.details = e.details
     97             self.extras = e.extras
     98         elif e:
     99             self.details = str(e)
    100 
    101     def testPass(self, e=None):
    102         """To mark the test as passed in this record.
    103 
    104         Args:
    105             e: An instance of vts.runners.host.signals.TestPass.
    106         """
    107         self._testEnd(TestResultEnums.TEST_RESULT_PASS, e)
    108 
    109     def testFail(self, e=None):
    110         """To mark the test as failed in this record.
    111 
    112         Only testFail does instance check because we want "assert xxx" to also
    113         fail the test same way assert_true does.
    114 
    115         Args:
    116             e: An exception object. It can be an instance of AssertionError or
    117                 vts.runners.host.base_test.TestFailure.
    118         """
    119         self._testEnd(TestResultEnums.TEST_RESULT_FAIL, e)
    120 
    121     def testSkip(self, e=None):
    122         """To mark the test as skipped in this record.
    123 
    124         Args:
    125             e: An instance of vts.runners.host.signals.TestSkip.
    126         """
    127         self._testEnd(TestResultEnums.TEST_RESULT_SKIP, e)
    128 
    129     def testError(self, e=None):
    130         """To mark the test as error in this record.
    131 
    132         Args:
    133             e: An exception object.
    134         """
    135         self._testEnd(TestResultEnums.TEST_RESULT_ERROR, e)
    136 
    137     def addError(self, tag, e):
    138         """Add extra error happened during a test mark the test result as
    139         ERROR.
    140 
    141         If an error is added the test record, the record's result is equivalent
    142         to the case where an uncaught exception happened.
    143 
    144         Args:
    145             tag: A string describing where this error came from, e.g. 'on_pass'.
    146             e: An exception object.
    147         """
    148         self.result = TestResultEnums.TEST_RESULT_ERROR
    149         self.extra_errors[tag] = str(e)
    150 
    151     def addTable(self, name, rows):
    152         """Add a table as part of the test result.
    153 
    154         Args:
    155             name: The table name.
    156             rows: A 2-dimensional list which contains the data.
    157         """
    158         if name in self.tables:
    159             logging.warning("Overwrite table %s" % name)
    160         self.tables[name] = rows
    161 
    162     def __str__(self):
    163         d = self.getDict()
    164         l = ["%s = %s" % (k, v) for k, v in d.items()]
    165         s = ', '.join(l)
    166         return s
    167 
    168     def __repr__(self):
    169         """This returns a short string representation of the test record."""
    170         t = utils.epoch_to_human_time(self.begin_time)
    171         return "%s %s %s" % (t, self.test_name, self.result)
    172 
    173     def getDict(self):
    174         """Gets a dictionary representating the content of this class.
    175 
    176         Returns:
    177             A dictionary representating the content of this class.
    178         """
    179         d = {}
    180         d[TestResultEnums.RECORD_NAME] = self.test_name
    181         d[TestResultEnums.RECORD_CLASS] = self.test_class
    182         d[TestResultEnums.RECORD_BEGIN_TIME] = self.begin_time
    183         d[TestResultEnums.RECORD_END_TIME] = self.end_time
    184         d[TestResultEnums.RECORD_RESULT] = self.result
    185         d[TestResultEnums.RECORD_UID] = self.uid
    186         d[TestResultEnums.RECORD_EXTRAS] = self.extras
    187         d[TestResultEnums.RECORD_DETAILS] = self.details
    188         d[TestResultEnums.RECORD_EXTRA_ERRORS] = self.extra_errors
    189         d[TestResultEnums.RECORD_TABLES] = self.tables
    190         return d
    191 
    192     def jsonString(self):
    193         """Converts this test record to a string in json format.
    194 
    195         Format of the json string is:
    196             {
    197                 'Test Name': <test name>,
    198                 'Begin Time': <epoch timestamp>,
    199                 'Details': <details>,
    200                 ...
    201             }
    202 
    203         Returns:
    204             A json-format string representing the test record.
    205         """
    206         return json.dumps(self.getDict())
    207 
    208 
    209 class TestResult(object):
    210     """A class that contains metrics of a test run.
    211 
    212     This class is essentially a container of TestResultRecord objects.
    213 
    214     Attributes:
    215         self.requested: A list of records for tests requested by user.
    216         self.failed: A list of records for tests failed.
    217         self.executed: A list of records for tests that were actually executed.
    218         self.passed: A list of records for tests passed.
    219         self.skipped: A list of records for tests skipped.
    220         self.error: A list of records for tests with error result token.
    221         self._test_module_name: A string, test module's name.
    222         self._test_module_timestamp: An integer, test module's execution start
    223                                      timestamp.
    224     """
    225 
    226     def __init__(self):
    227         self.requested = []
    228         self.failed = []
    229         self.executed = []
    230         self.passed = []
    231         self.skipped = []
    232         self.error = []
    233         self._test_module_name = None
    234         self._test_module_timestamp = None
    235 
    236     def __add__(self, r):
    237         """Overrides '+' operator for TestResult class.
    238 
    239         The add operator merges two TestResult objects by concatenating all of
    240         their lists together.
    241 
    242         Args:
    243             r: another instance of TestResult to be added
    244 
    245         Returns:
    246             A TestResult instance that's the sum of two TestResult instances.
    247         """
    248         if not isinstance(r, TestResult):
    249             raise TypeError("Operand %s of type %s is not a TestResult." %
    250                             (r, type(r)))
    251         r.reportNonExecutedRecord()
    252         sum_result = TestResult()
    253         for name in sum_result.__dict__:
    254             if name.startswith("_test_module"):
    255                 l_value = getattr(self, name)
    256                 r_value = getattr(r, name)
    257                 if l_value is None and r_value is None:
    258                     continue
    259                 elif l_value is None and r_value is not None:
    260                     value = r_value
    261                 elif l_value is not None and r_value is None:
    262                     value = l_value
    263                 else:
    264                     if name == "_test_module_name":
    265                         if l_value != r_value:
    266                             raise TypeError("_test_module_name is different.")
    267                         value = l_value
    268                     elif name == "_test_module_timestamp":
    269                         if int(l_value) < int(r_value):
    270                             value = l_value
    271                         else:
    272                             value = r_value
    273                     else:
    274                         raise TypeError("unknown _test_module* attribute.")
    275                 setattr(sum_result, name, value)
    276             else:
    277                 l_value = list(getattr(self, name))
    278                 r_value = list(getattr(r, name))
    279                 setattr(sum_result, name, l_value + r_value)
    280         return sum_result
    281 
    282     def reportNonExecutedRecord(self):
    283         """Check and report any requested tests that did not finish.
    284 
    285         Adds a test record to self.error list iff it is in requested list but not
    286         self.executed result list.
    287         """
    288         for requested in self.requested:
    289             found = False
    290 
    291             for executed in self.executed:
    292                 if (requested.test_name == executed.test_name and
    293                         requested.test_class == executed.test_class):
    294                     found = True
    295                     break
    296 
    297             if not found:
    298                 requested.testBegin()
    299                 requested.testError()
    300                 self.error.append(requested)
    301 
    302     def removeRecord(self, record):
    303         """Remove a test record from test results.
    304 
    305         Records will be ed using test_name and test_class attribute.
    306         All entries that match the provided record in all result lists will
    307         be removed after calling this method.
    308 
    309         Args:
    310             record: A test record object to add.
    311         """
    312         lists = [
    313             self.requested, self.failed, self.executed, self.passed,
    314             self.skipped, self.error
    315         ]
    316 
    317         for l in lists:
    318             indexToRemove = []
    319             for idx in range(len(l)):
    320                 if (l[idx].test_name == record.test_name and
    321                         l[idx].test_class == record.test_class):
    322                     indexToRemove.append(idx)
    323 
    324             for idx in reversed(indexToRemove):
    325                 del l[idx]
    326 
    327     def addRecord(self, record):
    328         """Adds a test record to test results.
    329 
    330         A record is considered executed once it's added to the test result.
    331 
    332         Args:
    333             record: A test record object to add.
    334         """
    335         self.executed.append(record)
    336         if record.result == TestResultEnums.TEST_RESULT_FAIL:
    337             self.failed.append(record)
    338         elif record.result == TestResultEnums.TEST_RESULT_SKIP:
    339             self.skipped.append(record)
    340         elif record.result == TestResultEnums.TEST_RESULT_PASS:
    341             self.passed.append(record)
    342         else:
    343             self.error.append(record)
    344 
    345     def setTestModuleKeys(self, name, start_timestamp):
    346         """Sets the test module's name and start_timestamp."""
    347         self._test_module_name = name
    348         self._test_module_timestamp = start_timestamp
    349 
    350     def failClass(self, class_name, e):
    351         """Add a record to indicate a test class setup has failed and no test
    352         in the class was executed.
    353 
    354         Args:
    355             class_name: A string that is the name of the failed test class.
    356             e: An exception object.
    357         """
    358         record = TestResultRecord("setup_class", class_name)
    359         record.testBegin()
    360         record.testFail(e)
    361         self.executed.append(record)
    362         self.failed.append(record)
    363 
    364     def skipClass(self, class_name, reason):
    365         """Add a record to indicate all test cases in the class are skipped.
    366 
    367         Args:
    368             class_name: A string that is the name of the skipped test class.
    369             reason: A string that is the reason for skipping.
    370         """
    371         record = TestResultRecord("unknown", class_name)
    372         record.testBegin()
    373         record.testSkip(signals.TestSkip(reason))
    374         self.executed.append(record)
    375         self.skipped.append(record)
    376 
    377     def jsonString(self):
    378         """Converts this test result to a string in json format.
    379 
    380         Format of the json string is:
    381             {
    382                 "Results": [
    383                     {<executed test record 1>},
    384                     {<executed test record 2>},
    385                     ...
    386                 ],
    387                 "Summary": <summary dict>
    388             }
    389 
    390         Returns:
    391             A json-format string representing the test results.
    392         """
    393         records = list_utils.MergeUniqueKeepOrder(
    394             self.executed, self.failed, self.passed, self.skipped, self.error)
    395         executed = [record.getDict() for record in records]
    396 
    397         d = {}
    398         d["Results"] = executed
    399         d["Summary"] = self.summaryDict()
    400         d["TestModule"] = self.testModuleDict()
    401         jsonString = json.dumps(d, indent=4, sort_keys=True)
    402         return jsonString
    403 
    404     def summary(self):
    405         """Gets a string that summarizes the stats of this test result.
    406 
    407         The summary rovides the counts of how many test cases fall into each
    408         category, like "Passed", "Failed" etc.
    409 
    410         Format of the string is:
    411             Requested <int>, Executed <int>, ...
    412 
    413         Returns:
    414             A summary string of this test result.
    415         """
    416         l = ["%s %d" % (k, v) for k, v in self.summaryDict().items()]
    417         # Sort the list so the order is the same every time.
    418         msg = ", ".join(sorted(l))
    419         return msg
    420 
    421     def summaryDict(self):
    422         """Gets a dictionary that summarizes the stats of this test result.
    423 
    424         The summary rovides the counts of how many test cases fall into each
    425         category, like "Passed", "Failed" etc.
    426 
    427         Returns:
    428             A dictionary with the stats of this test result.
    429         """
    430         d = {}
    431         d["Requested"] = len(self.requested)
    432         d["Executed"] = len(self.executed)
    433         d["Passed"] = len(self.passed)
    434         d["Failed"] = len(self.failed)
    435         d["Skipped"] = len(self.skipped)
    436         d["Error"] = len(self.error)
    437         return d
    438 
    439     def testModuleDict(self):
    440         """Returns a dict that summarizes the test module DB indexing keys."""
    441         d = {}
    442         d["Name"] = self._test_module_name
    443         d["Timestamp"] = self._test_module_timestamp
    444         return d
    445