mirror of https://github.com/F-Stack/f-stack.git
328 lines
10 KiB
Python
328 lines
10 KiB
Python
|
# SPDX-License-Identifier: BSD-3-Clause
|
||
|
# Copyright(c) 2023 PANTHEON.tech s.r.o.
|
||
|
# Copyright(c) 2023 University of New Hampshire
|
||
|
|
||
|
"""
|
||
|
Generic result container and reporters
|
||
|
"""
|
||
|
|
||
|
import os.path
|
||
|
from collections.abc import MutableSequence
|
||
|
from enum import Enum, auto
|
||
|
|
||
|
from .config import (
|
||
|
OS,
|
||
|
Architecture,
|
||
|
BuildTargetConfiguration,
|
||
|
BuildTargetInfo,
|
||
|
Compiler,
|
||
|
CPUType,
|
||
|
NodeConfiguration,
|
||
|
NodeInfo,
|
||
|
)
|
||
|
from .exception import DTSError, ErrorSeverity
|
||
|
from .logger import DTSLOG
|
||
|
from .settings import SETTINGS
|
||
|
|
||
|
|
||
|
class Result(Enum):
|
||
|
"""
|
||
|
An Enum defining the possible states that
|
||
|
a setup, a teardown or a test case may end up in.
|
||
|
"""
|
||
|
|
||
|
PASS = auto()
|
||
|
FAIL = auto()
|
||
|
ERROR = auto()
|
||
|
SKIP = auto()
|
||
|
|
||
|
def __bool__(self) -> bool:
|
||
|
return self is self.PASS
|
||
|
|
||
|
|
||
|
class FixtureResult(object):
|
||
|
"""
|
||
|
A record that stored the result of a setup or a teardown.
|
||
|
The default is FAIL because immediately after creating the object
|
||
|
the setup of the corresponding stage will be executed, which also guarantees
|
||
|
the execution of teardown.
|
||
|
"""
|
||
|
|
||
|
result: Result
|
||
|
error: Exception | None = None
|
||
|
|
||
|
def __init__(
|
||
|
self,
|
||
|
result: Result = Result.FAIL,
|
||
|
error: Exception | None = None,
|
||
|
):
|
||
|
self.result = result
|
||
|
self.error = error
|
||
|
|
||
|
def __bool__(self) -> bool:
|
||
|
return bool(self.result)
|
||
|
|
||
|
|
||
|
class Statistics(dict):
|
||
|
"""
|
||
|
A helper class used to store the number of test cases by its result
|
||
|
along a few other basic information.
|
||
|
Using a dict provides a convenient way to format the data.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, dpdk_version: str | None):
|
||
|
super(Statistics, self).__init__()
|
||
|
for result in Result:
|
||
|
self[result.name] = 0
|
||
|
self["PASS RATE"] = 0.0
|
||
|
self["DPDK VERSION"] = dpdk_version
|
||
|
|
||
|
def __iadd__(self, other: Result) -> "Statistics":
|
||
|
"""
|
||
|
Add a Result to the final count.
|
||
|
"""
|
||
|
self[other.name] += 1
|
||
|
self["PASS RATE"] = (
|
||
|
float(self[Result.PASS.name]) * 100 / sum(self[result.name] for result in Result)
|
||
|
)
|
||
|
return self
|
||
|
|
||
|
def __str__(self) -> str:
|
||
|
"""
|
||
|
Provide a string representation of the data.
|
||
|
"""
|
||
|
stats_str = ""
|
||
|
for key, value in self.items():
|
||
|
stats_str += f"{key:<12} = {value}\n"
|
||
|
# according to docs, we should use \n when writing to text files
|
||
|
# on all platforms
|
||
|
return stats_str
|
||
|
|
||
|
|
||
|
class BaseResult(object):
|
||
|
"""
|
||
|
The Base class for all results. Stores the results of
|
||
|
the setup and teardown portions of the corresponding stage
|
||
|
and a list of results from each inner stage in _inner_results.
|
||
|
"""
|
||
|
|
||
|
setup_result: FixtureResult
|
||
|
teardown_result: FixtureResult
|
||
|
_inner_results: MutableSequence["BaseResult"]
|
||
|
|
||
|
def __init__(self):
|
||
|
self.setup_result = FixtureResult()
|
||
|
self.teardown_result = FixtureResult()
|
||
|
self._inner_results = []
|
||
|
|
||
|
def update_setup(self, result: Result, error: Exception | None = None) -> None:
|
||
|
self.setup_result.result = result
|
||
|
self.setup_result.error = error
|
||
|
|
||
|
def update_teardown(self, result: Result, error: Exception | None = None) -> None:
|
||
|
self.teardown_result.result = result
|
||
|
self.teardown_result.error = error
|
||
|
|
||
|
def _get_setup_teardown_errors(self) -> list[Exception]:
|
||
|
errors = []
|
||
|
if self.setup_result.error:
|
||
|
errors.append(self.setup_result.error)
|
||
|
if self.teardown_result.error:
|
||
|
errors.append(self.teardown_result.error)
|
||
|
return errors
|
||
|
|
||
|
def _get_inner_errors(self) -> list[Exception]:
|
||
|
return [
|
||
|
error for inner_result in self._inner_results for error in inner_result.get_errors()
|
||
|
]
|
||
|
|
||
|
def get_errors(self) -> list[Exception]:
|
||
|
return self._get_setup_teardown_errors() + self._get_inner_errors()
|
||
|
|
||
|
def add_stats(self, statistics: Statistics) -> None:
|
||
|
for inner_result in self._inner_results:
|
||
|
inner_result.add_stats(statistics)
|
||
|
|
||
|
|
||
|
class TestCaseResult(BaseResult, FixtureResult):
|
||
|
"""
|
||
|
The test case specific result.
|
||
|
Stores the result of the actual test case.
|
||
|
Also stores the test case name.
|
||
|
"""
|
||
|
|
||
|
test_case_name: str
|
||
|
|
||
|
def __init__(self, test_case_name: str):
|
||
|
super(TestCaseResult, self).__init__()
|
||
|
self.test_case_name = test_case_name
|
||
|
|
||
|
def update(self, result: Result, error: Exception | None = None) -> None:
|
||
|
self.result = result
|
||
|
self.error = error
|
||
|
|
||
|
def _get_inner_errors(self) -> list[Exception]:
|
||
|
if self.error:
|
||
|
return [self.error]
|
||
|
return []
|
||
|
|
||
|
def add_stats(self, statistics: Statistics) -> None:
|
||
|
statistics += self.result
|
||
|
|
||
|
def __bool__(self) -> bool:
|
||
|
return bool(self.setup_result) and bool(self.teardown_result) and bool(self.result)
|
||
|
|
||
|
|
||
|
class TestSuiteResult(BaseResult):
|
||
|
"""
|
||
|
The test suite specific result.
|
||
|
The _inner_results list stores results of test cases in a given test suite.
|
||
|
Also stores the test suite name.
|
||
|
"""
|
||
|
|
||
|
suite_name: str
|
||
|
|
||
|
def __init__(self, suite_name: str):
|
||
|
super(TestSuiteResult, self).__init__()
|
||
|
self.suite_name = suite_name
|
||
|
|
||
|
def add_test_case(self, test_case_name: str) -> TestCaseResult:
|
||
|
test_case_result = TestCaseResult(test_case_name)
|
||
|
self._inner_results.append(test_case_result)
|
||
|
return test_case_result
|
||
|
|
||
|
|
||
|
class BuildTargetResult(BaseResult):
|
||
|
"""
|
||
|
The build target specific result.
|
||
|
The _inner_results list stores results of test suites in a given build target.
|
||
|
Also stores build target specifics, such as compiler used to build DPDK.
|
||
|
"""
|
||
|
|
||
|
arch: Architecture
|
||
|
os: OS
|
||
|
cpu: CPUType
|
||
|
compiler: Compiler
|
||
|
compiler_version: str | None
|
||
|
dpdk_version: str | None
|
||
|
|
||
|
def __init__(self, build_target: BuildTargetConfiguration):
|
||
|
super(BuildTargetResult, self).__init__()
|
||
|
self.arch = build_target.arch
|
||
|
self.os = build_target.os
|
||
|
self.cpu = build_target.cpu
|
||
|
self.compiler = build_target.compiler
|
||
|
self.compiler_version = None
|
||
|
self.dpdk_version = None
|
||
|
|
||
|
def add_build_target_info(self, versions: BuildTargetInfo) -> None:
|
||
|
self.compiler_version = versions.compiler_version
|
||
|
self.dpdk_version = versions.dpdk_version
|
||
|
|
||
|
def add_test_suite(self, test_suite_name: str) -> TestSuiteResult:
|
||
|
test_suite_result = TestSuiteResult(test_suite_name)
|
||
|
self._inner_results.append(test_suite_result)
|
||
|
return test_suite_result
|
||
|
|
||
|
|
||
|
class ExecutionResult(BaseResult):
|
||
|
"""
|
||
|
The execution specific result.
|
||
|
The _inner_results list stores results of build targets in a given execution.
|
||
|
Also stores the SUT node configuration.
|
||
|
"""
|
||
|
|
||
|
sut_node: NodeConfiguration
|
||
|
sut_os_name: str
|
||
|
sut_os_version: str
|
||
|
sut_kernel_version: str
|
||
|
|
||
|
def __init__(self, sut_node: NodeConfiguration):
|
||
|
super(ExecutionResult, self).__init__()
|
||
|
self.sut_node = sut_node
|
||
|
|
||
|
def add_build_target(self, build_target: BuildTargetConfiguration) -> BuildTargetResult:
|
||
|
build_target_result = BuildTargetResult(build_target)
|
||
|
self._inner_results.append(build_target_result)
|
||
|
return build_target_result
|
||
|
|
||
|
def add_sut_info(self, sut_info: NodeInfo):
|
||
|
self.sut_os_name = sut_info.os_name
|
||
|
self.sut_os_version = sut_info.os_version
|
||
|
self.sut_kernel_version = sut_info.kernel_version
|
||
|
|
||
|
|
||
|
class DTSResult(BaseResult):
|
||
|
"""
|
||
|
Stores environment information and test results from a DTS run, which are:
|
||
|
* Execution level information, such as SUT and TG hardware.
|
||
|
* Build target level information, such as compiler, target OS and cpu.
|
||
|
* Test suite results.
|
||
|
* All errors that are caught and recorded during DTS execution.
|
||
|
|
||
|
The information is stored in nested objects.
|
||
|
|
||
|
The class is capable of computing the return code used to exit DTS with
|
||
|
from the stored error.
|
||
|
|
||
|
It also provides a brief statistical summary of passed/failed test cases.
|
||
|
"""
|
||
|
|
||
|
dpdk_version: str | None
|
||
|
_logger: DTSLOG
|
||
|
_errors: list[Exception]
|
||
|
_return_code: ErrorSeverity
|
||
|
_stats_result: Statistics | None
|
||
|
_stats_filename: str
|
||
|
|
||
|
def __init__(self, logger: DTSLOG):
|
||
|
super(DTSResult, self).__init__()
|
||
|
self.dpdk_version = None
|
||
|
self._logger = logger
|
||
|
self._errors = []
|
||
|
self._return_code = ErrorSeverity.NO_ERR
|
||
|
self._stats_result = None
|
||
|
self._stats_filename = os.path.join(SETTINGS.output_dir, "statistics.txt")
|
||
|
|
||
|
def add_execution(self, sut_node: NodeConfiguration) -> ExecutionResult:
|
||
|
execution_result = ExecutionResult(sut_node)
|
||
|
self._inner_results.append(execution_result)
|
||
|
return execution_result
|
||
|
|
||
|
def add_error(self, error) -> None:
|
||
|
self._errors.append(error)
|
||
|
|
||
|
def process(self) -> None:
|
||
|
"""
|
||
|
Process the data after a DTS run.
|
||
|
The data is added to nested objects during runtime and this parent object
|
||
|
is not updated at that time. This requires us to process the nested data
|
||
|
after it's all been gathered.
|
||
|
|
||
|
The processing gathers all errors and the result statistics of test cases.
|
||
|
"""
|
||
|
self._errors += self.get_errors()
|
||
|
if self._errors and self._logger:
|
||
|
self._logger.debug("Summary of errors:")
|
||
|
for error in self._errors:
|
||
|
self._logger.debug(repr(error))
|
||
|
|
||
|
self._stats_result = Statistics(self.dpdk_version)
|
||
|
self.add_stats(self._stats_result)
|
||
|
with open(self._stats_filename, "w+") as stats_file:
|
||
|
stats_file.write(str(self._stats_result))
|
||
|
|
||
|
def get_return_code(self) -> int:
|
||
|
"""
|
||
|
Go through all stored Exceptions and return the highest error code found.
|
||
|
"""
|
||
|
for error in self._errors:
|
||
|
error_return_code = ErrorSeverity.GENERIC_ERR
|
||
|
if isinstance(error, DTSError):
|
||
|
error_return_code = error.severity
|
||
|
|
||
|
if error_return_code > self._return_code:
|
||
|
self._return_code = error_return_code
|
||
|
|
||
|
return int(self._return_code)
|