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.

506 lines
17 KiB

#
# Copyright (C) 2016 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from __builtin__ import property
"""This module is where all the record definitions and record containers live.
"""
import json
import logging
import pprint
from vts.runners.host import signals
from vts.runners.host import utils
from vts.utils.python.common import list_utils
class TestResultEnums(object):
"""Enums used for TestResultRecord class.
Includes the tokens to mark test result with, and the string names for each
field in TestResultRecord.
"""
RECORD_NAME = "Test Name"
RECORD_CLASS = "Test Class"
RECORD_BEGIN_TIME = "Begin Time"
RECORD_END_TIME = "End Time"
RECORD_RESULT = "Result"
RECORD_UID = "UID"
RECORD_EXTRAS = "Extras"
RECORD_EXTRA_ERRORS = "Extra Errors"
RECORD_DETAILS = "Details"
RECORD_TABLES = "Tables"
TEST_RESULT_PASS = "PASS"
TEST_RESULT_FAIL = "FAIL"
TEST_RESULT_SKIP = "SKIP"
TEST_RESULT_ERROR = "ERROR"
class TestResultRecord(object):
"""A record that holds the information of a test case execution.
Attributes:
test_name: A string representing the name of the test case.
begin_time: Epoch timestamp of when the test case started.
end_time: Epoch timestamp of when the test case ended.
uid: Unique identifier of a test case.
result: Test result, PASS/FAIL/SKIP.
extras: User defined extra information of the test result.
details: A string explaining the details of the test case.
tables: A dict of 2-dimensional lists containing tabular results.
"""
def __init__(self, t_name, t_class=None):
self.test_name = t_name
self.test_class = t_class
self.begin_time = None
self.end_time = None
self.uid = None
self.result = None
self.extras = None
self.details = None
self.extra_errors = {}
self.tables = {}
@property
def fullname(self):
return '%s.%s' % (self.test_class, self.test_name)
def isSameTestCase(self, record):
return self.fullname == record.fullname
def testBegin(self):
"""Call this when the test case it records begins execution.
Sets the begin_time of this record.
"""
self.begin_time = utils.get_current_epoch_time()
def _testEnd(self, result, e):
"""Class internal function to signal the end of a test case execution.
Args:
result: One of the TEST_RESULT enums in TestResultEnums.
e: A test termination signal (usually an exception object). It can
be any exception instance or of any subclass of
vts.runners.host.signals.TestSignal.
"""
self.end_time = utils.get_current_epoch_time()
self.result = result
if isinstance(e, signals.TestSignal):
self.details = e.details
self.extras = e.extras
elif e:
self.details = str(e)
def testPass(self, e=None):
"""To mark the test as passed in this record.
Args:
e: An instance of vts.runners.host.signals.TestPass.
"""
self._testEnd(TestResultEnums.TEST_RESULT_PASS, e)
def testFail(self, e=None):
"""To mark the test as failed in this record.
Only testFail does instance check because we want "assert xxx" to also
fail the test same way assert_true does.
Args:
e: An exception object. It can be an instance of AssertionError or
vts.runners.host.base_test.TestFailure.
"""
self._testEnd(TestResultEnums.TEST_RESULT_FAIL, e)
def testSkip(self, e=None):
"""To mark the test as skipped in this record.
Args:
e: An instance of vts.runners.host.signals.TestSkip.
"""
self._testEnd(TestResultEnums.TEST_RESULT_SKIP, e)
def testError(self, e=None):
"""To mark the test as error in this record.
Args:
e: An exception object.
"""
self._testEnd(TestResultEnums.TEST_RESULT_ERROR, e)
def addError(self, tag, e):
"""Add extra error happened during a test mark the test result as
ERROR.
If an error is added the test record, the record's result is equivalent
to the case where an uncaught exception happened.
Args:
tag: A string describing where this error came from, e.g. 'on_pass'.
e: An exception object.
"""
self.result = TestResultEnums.TEST_RESULT_ERROR
self.extra_errors[tag] = str(e)
def addTable(self, name, rows):
"""Add a table as part of the test result.
Args:
name: The table name.
rows: A 2-dimensional list which contains the data.
"""
if name in self.tables:
logging.warning("Overwrite table %s" % name)
self.tables[name] = rows
def __str__(self):
d = self.getDict()
l = ["%s = %s" % (k, v) for k, v in d.items()]
s = ', '.join(l)
return s
def __repr__(self):
"""This returns a short string representation of the test record."""
t = utils.epoch_to_human_time(self.begin_time)
return "%s %s %s" % (t, self.test_name, self.result)
def getDict(self):
"""Gets a dictionary representating the content of this class.
Returns:
A dictionary representating the content of this class.
"""
d = {}
d[TestResultEnums.RECORD_NAME] = self.test_name
d[TestResultEnums.RECORD_CLASS] = self.test_class
d[TestResultEnums.RECORD_BEGIN_TIME] = self.begin_time
d[TestResultEnums.RECORD_END_TIME] = self.end_time
d[TestResultEnums.RECORD_RESULT] = self.result
d[TestResultEnums.RECORD_UID] = self.uid
d[TestResultEnums.RECORD_EXTRAS] = self.extras
d[TestResultEnums.RECORD_DETAILS] = self.details
d[TestResultEnums.RECORD_EXTRA_ERRORS] = self.extra_errors
d[TestResultEnums.RECORD_TABLES] = self.tables
return d
def jsonString(self):
"""Converts this test record to a string in json format.
Format of the json string is:
{
'Test Name': <test name>,
'Begin Time': <epoch timestamp>,
'Details': <details>,
...
}
Returns:
A json-format string representing the test record.
"""
return json.dumps(self.getDict())
class TestResult(object):
"""A class that contains metrics of a test run.
This class is essentially a container of TestResultRecord objects.
Attributes:
self.requested: A list of records for tests requested by user.
self.failed: A list of records for tests failed.
self.executed: A list of records for tests that were actually executed.
self.passed: A list of records for tests passed.
self.skipped: A list of records for tests skipped.
self.error: A list of records for tests with error result token.
self.class_errors: A list of strings, the errors that occurred during
class setup.
self._test_module_name: A string, test module's name.
self._test_module_timestamp: An integer, test module's execution start
timestamp.
"""
def __init__(self):
self.requested = []
self.failed = []
self.executed = []
self.passed = []
self.skipped = []
self.error = []
self._test_module_name = None
self._test_module_timestamp = None
self.class_errors = []
def __add__(self, r):
"""Overrides '+' operator for TestResult class.
The add operator merges two TestResult objects by concatenating all of
their lists together.
Args:
r: another instance of TestResult to be added
Returns:
A TestResult instance that's the sum of two TestResult instances.
"""
if not isinstance(r, TestResult):
raise TypeError("Operand %s of type %s is not a TestResult." %
(r, type(r)))
r.reportNonExecutedRecord()
sum_result = TestResult()
for name in sum_result.__dict__:
if name.startswith("_test_module"):
l_value = getattr(self, name)
r_value = getattr(r, name)
if l_value is None and r_value is None:
continue
elif l_value is None and r_value is not None:
value = r_value
elif l_value is not None and r_value is None:
value = l_value
else:
if name == "_test_module_name":
if l_value != r_value:
raise TypeError("_test_module_name is different.")
value = l_value
elif name == "_test_module_timestamp":
if int(l_value) < int(r_value):
value = l_value
else:
value = r_value
else:
raise TypeError("unknown _test_module* attribute.")
setattr(sum_result, name, value)
else:
l_value = list(getattr(self, name))
r_value = list(getattr(r, name))
setattr(sum_result, name, l_value + r_value)
return sum_result
def getNonPassingRecords(self, non_executed=True, failed=True, skipped=False, error=True):
"""Returns a list of non-passing test records.
Returns:
a list of TestResultRecord, records that do not have passing result.
non_executed: bool, whether to include non-executed results
failed: bool, whether to include failed results
skipped: bool, whether to include skipped results
error: bool, whether to include error results
"""
return ((self.getNonExecutedRecords() if non_executed else [])
+ (self.failed if failed else [])
+ (self.skipped if skipped else [])
+ (self.error if error else []))
def getNonExecutedRecords(self):
"""Returns a list of records that were requested but not executed."""
res = []
for requested in self.requested:
found = False
for executed in self.executed:
if requested.isSameTestCase(executed):
found = True
break
if not found:
res.append(requested)
return res
def reportNonExecutedRecord(self):
"""Check and report any requested tests that did not finish.
Adds a test record to self.error list iff it is in requested list but not
self.executed result list.
"""
for requested in self.getNonExecutedRecords():
requested.testBegin()
requested.testError(
"Unknown error: test case requested but not executed.")
self.error.append(requested)
def removeRecord(self, record, remove_requested=True):
"""Remove a test record from test results.
Records will be ed using test_name and test_class attribute.
All entries that match the provided record in all result lists will
be removed after calling this method.
Args:
record: A test record object to add.
remove_requested: bool, whether to remove the test case from requested
list as well.
"""
lists = [
self.requested, self.failed, self.executed, self.passed,
self.skipped, self.error
]
for l in lists:
indexToRemove = []
for idx in range(len(l)):
if l[idx].isSameTestCase(record):
indexToRemove.append(idx)
for idx in reversed(indexToRemove):
del l[idx]
def addRecord(self, record):
"""Adds a test record to test results.
A record is considered executed once it's added to the test result.
Args:
record: A test record object to add.
"""
self.removeRecord(record, remove_requested=False)
self.executed.append(record)
if record.result == TestResultEnums.TEST_RESULT_FAIL:
self.failed.append(record)
elif record.result == TestResultEnums.TEST_RESULT_SKIP:
self.skipped.append(record)
elif record.result == TestResultEnums.TEST_RESULT_PASS:
self.passed.append(record)
else:
self.error.append(record)
def setTestModuleKeys(self, name, start_timestamp):
"""Sets the test module's name and start_timestamp."""
self._test_module_name = name
self._test_module_timestamp = start_timestamp
def failClass(self, class_name, e):
"""Add a record to indicate a test class setup has failed and no test
in the class was executed.
Args:
class_name: A string that is the name of the failed test class.
e: An exception object.
"""
self.class_errors.append("%s: %s" % (class_name, e))
def passClass(self, class_name, e=None):
"""Add a record to indicate a test class setup has passed and no test
in the class was executed.
Args:
class_name: A string that is the name of the failed test class.
e: An exception object.
"""
record = TestResultRecord("setup_class", class_name)
record.testBegin()
record.testPass(e)
self.executed.append(record)
self.passed.append(record)
def skipClass(self, class_name, reason):
"""Add a record to indicate all test cases in the class are skipped.
Args:
class_name: A string that is the name of the skipped test class.
reason: A string that is the reason for skipping.
"""
record = TestResultRecord("skip_class", class_name)
record.testBegin()
record.testSkip(signals.TestSkip(reason))
self.executed.append(record)
self.skipped.append(record)
def jsonString(self):
"""Converts this test result to a string in json format.
Format of the json string is:
{
"Results": [
{<executed test record 1>},
{<executed test record 2>},
...
],
"Summary": <summary dict>
}
Returns:
A json-format string representing the test results.
"""
records = list_utils.MergeUniqueKeepOrder(
self.executed, self.failed, self.passed, self.skipped, self.error)
executed = [record.getDict() for record in records]
d = {}
d["Results"] = executed
d["Summary"] = self.summaryDict()
d["TestModule"] = self.testModuleDict()
d["Class Errors"] = ("\n".join(self.class_errors)
if self.class_errors else None)
jsonString = json.dumps(d, indent=4, sort_keys=True)
return jsonString
def summary(self):
"""Gets a string that summarizes the stats of this test result.
The summary rovides the counts of how many test cases fall into each
category, like "Passed", "Failed" etc.
Format of the string is:
Requested <int>, Executed <int>, ...
Returns:
A summary string of this test result.
"""
l = ["%s %d" % (k, v) for k, v in self.summaryDict().items()]
# Sort the list so the order is the same every time.
msg = ", ".join(sorted(l))
return msg
@property
def progressStr(self):
"""Gets a string that shows test progress.
Format of the string is:
x/n, where x is number of executed + skipped + 1,
and n is number of requested tests.
"""
return '%s/%s' % (len(self.executed) + len(self.skipped) + 1,
len(self.requested))
def summaryDict(self):
"""Gets a dictionary that summarizes the stats of this test result.
The summary rovides the counts of how many test cases fall into each
category, like "Passed", "Failed" etc.
Returns:
A dictionary with the stats of this test result.
"""
d = {}
d["Requested"] = len(self.requested)
d["Executed"] = len(self.executed)
d["Passed"] = len(self.passed)
d["Failed"] = len(self.failed)
d["Skipped"] = len(self.skipped)
d["Error"] = len(self.error)
return d
def testModuleDict(self):
"""Returns a dict that summarizes the test module DB indexing keys."""
d = {}
d["Name"] = self._test_module_name
d["Timestamp"] = self._test_module_timestamp
return d