10c6f2d11SJuraj Linkeš# SPDX-License-Identifier: BSD-3-Clause 20c6f2d11SJuraj Linkeš# Copyright(c) 2023 PANTHEON.tech s.r.o. 388489c05SJeremy Spewock# Copyright(c) 2023 University of New Hampshire 4*9f8a2572STomáš Ďurovec# Copyright(c) 2024 Arm Limited 50c6f2d11SJuraj Linkeš 66ef07151SJuraj Linkešr"""Record and process DTS results. 76ef07151SJuraj Linkeš 86ef07151SJuraj LinkešThe results are recorded in a hierarchical manner: 96ef07151SJuraj Linkeš 106ef07151SJuraj Linkeš * :class:`DTSResult` contains 1185ceeeceSJuraj Linkeš * :class:`TestRunResult` contains 126ef07151SJuraj Linkeš * :class:`TestSuiteResult` contains 136ef07151SJuraj Linkeš * :class:`TestCaseResult` 146ef07151SJuraj Linkeš 156ef07151SJuraj LinkešEach result may contain multiple lower level results, e.g. there are multiple 1611b2279aSTomáš Ďurovec:class:`TestSuiteResult`\s in a :class:`TestRunResult`. 176ef07151SJuraj LinkešThe results have common parts, such as setup and teardown results, captured in :class:`BaseResult`, 186ef07151SJuraj Linkešwhich also defines some common behaviors in its methods. 196ef07151SJuraj Linkeš 206ef07151SJuraj LinkešEach result class has its own idiosyncrasies which they implement in overridden methods. 216ef07151SJuraj Linkeš 226ef07151SJuraj LinkešThe :option:`--output` command line argument and the :envvar:`DTS_OUTPUT_DIR` environment 236ef07151SJuraj Linkešvariable modify the directory where the files with results will be stored. 240c6f2d11SJuraj Linkeš""" 250c6f2d11SJuraj Linkeš 26*9f8a2572STomáš Ďurovecimport json 270c6f2d11SJuraj Linkešfrom collections.abc import MutableSequence 28*9f8a2572STomáš Ďurovecfrom dataclasses import asdict, dataclass, field 290c6f2d11SJuraj Linkešfrom enum import Enum, auto 30*9f8a2572STomáš Ďurovecfrom pathlib import Path 31*9f8a2572STomáš Ďurovecfrom typing import Any, Callable, TypedDict 320c6f2d11SJuraj Linkeš 33eebfb5bbSJuraj Linkešfrom framework.testbed_model.capability import Capability 34eebfb5bbSJuraj Linkeš 35c72ff85dSLuca Vizzarrofrom .config import TestRunConfiguration, TestSuiteConfig 360c6f2d11SJuraj Linkešfrom .exception import DTSError, ErrorSeverity 3704f5a5a6SJuraj Linkešfrom .logger import DTSLogger 38b6eb5004SJuraj Linkešfrom .test_suite import TestCase, TestSuite 39c72ff85dSLuca Vizzarrofrom .testbed_model.os_session import OSSessionInfo 40*9f8a2572STomáš Ďurovecfrom .testbed_model.port import Port 41c72ff85dSLuca Vizzarrofrom .testbed_model.sut_node import DPDKBuildInfo 425d094f9fSJuraj Linkeš 435d094f9fSJuraj Linkeš 445d094f9fSJuraj Linkeš@dataclass(slots=True, frozen=True) 455d094f9fSJuraj Linkešclass TestSuiteWithCases: 465d094f9fSJuraj Linkeš """A test suite class with test case methods. 475d094f9fSJuraj Linkeš 485d094f9fSJuraj Linkeš An auxiliary class holding a test case class with test case methods. The intended use of this 495d094f9fSJuraj Linkeš class is to hold a subset of test cases (which could be all test cases) because we don't have 505d094f9fSJuraj Linkeš all the data to instantiate the class at the point of inspection. The knowledge of this subset 515d094f9fSJuraj Linkeš is needed in case an error occurs before the class is instantiated and we need to record 525d094f9fSJuraj Linkeš which test cases were blocked by the error. 535d094f9fSJuraj Linkeš 545d094f9fSJuraj Linkeš Attributes: 555d094f9fSJuraj Linkeš test_suite_class: The test suite class. 565d094f9fSJuraj Linkeš test_cases: The test case methods. 57eebfb5bbSJuraj Linkeš required_capabilities: The combined required capabilities of both the test suite 58eebfb5bbSJuraj Linkeš and the subset of test cases. 595d094f9fSJuraj Linkeš """ 605d094f9fSJuraj Linkeš 615d094f9fSJuraj Linkeš test_suite_class: type[TestSuite] 62b6eb5004SJuraj Linkeš test_cases: list[type[TestCase]] 63eebfb5bbSJuraj Linkeš required_capabilities: set[Capability] = field(default_factory=set, init=False) 64eebfb5bbSJuraj Linkeš 65eebfb5bbSJuraj Linkeš def __post_init__(self): 66eebfb5bbSJuraj Linkeš """Gather the required capabilities of the test suite and all test cases.""" 67eebfb5bbSJuraj Linkeš for test_object in [self.test_suite_class] + self.test_cases: 68eebfb5bbSJuraj Linkeš self.required_capabilities.update(test_object.required_capabilities) 695d094f9fSJuraj Linkeš 705d094f9fSJuraj Linkeš def create_config(self) -> TestSuiteConfig: 715d094f9fSJuraj Linkeš """Generate a :class:`TestSuiteConfig` from the stored test suite with test cases. 725d094f9fSJuraj Linkeš 735d094f9fSJuraj Linkeš Returns: 745d094f9fSJuraj Linkeš The :class:`TestSuiteConfig` representation. 755d094f9fSJuraj Linkeš """ 765d094f9fSJuraj Linkeš return TestSuiteConfig( 775d094f9fSJuraj Linkeš test_suite=self.test_suite_class.__name__, 785d094f9fSJuraj Linkeš test_cases=[test_case.__name__ for test_case in self.test_cases], 795d094f9fSJuraj Linkeš ) 800c6f2d11SJuraj Linkeš 81eebfb5bbSJuraj Linkeš def mark_skip_unsupported(self, supported_capabilities: set[Capability]) -> None: 82eebfb5bbSJuraj Linkeš """Mark the test suite and test cases to be skipped. 83eebfb5bbSJuraj Linkeš 84eebfb5bbSJuraj Linkeš The mark is applied if object to be skipped requires any capabilities and at least one of 85eebfb5bbSJuraj Linkeš them is not among `supported_capabilities`. 86eebfb5bbSJuraj Linkeš 87eebfb5bbSJuraj Linkeš Args: 88eebfb5bbSJuraj Linkeš supported_capabilities: The supported capabilities. 89eebfb5bbSJuraj Linkeš """ 90eebfb5bbSJuraj Linkeš for test_object in [self.test_suite_class, *self.test_cases]: 91eebfb5bbSJuraj Linkeš capabilities_not_supported = test_object.required_capabilities - supported_capabilities 92eebfb5bbSJuraj Linkeš if capabilities_not_supported: 93eebfb5bbSJuraj Linkeš test_object.skip = True 94eebfb5bbSJuraj Linkeš capability_str = ( 95eebfb5bbSJuraj Linkeš "capability" if len(capabilities_not_supported) == 1 else "capabilities" 96eebfb5bbSJuraj Linkeš ) 97eebfb5bbSJuraj Linkeš test_object.skip_reason = ( 98eebfb5bbSJuraj Linkeš f"Required {capability_str} '{capabilities_not_supported}' not found." 99eebfb5bbSJuraj Linkeš ) 100eebfb5bbSJuraj Linkeš if not self.test_suite_class.skip: 101eebfb5bbSJuraj Linkeš if all(test_case.skip for test_case in self.test_cases): 102eebfb5bbSJuraj Linkeš self.test_suite_class.skip = True 103eebfb5bbSJuraj Linkeš 104eebfb5bbSJuraj Linkeš self.test_suite_class.skip_reason = ( 105eebfb5bbSJuraj Linkeš "All test cases are marked to be skipped with reasons: " 106eebfb5bbSJuraj Linkeš f"{' '.join(test_case.skip_reason for test_case in self.test_cases)}" 107eebfb5bbSJuraj Linkeš ) 108eebfb5bbSJuraj Linkeš 109566201aeSJuraj Linkeš @property 110566201aeSJuraj Linkeš def skip(self) -> bool: 111566201aeSJuraj Linkeš """Skip the test suite if all test cases or the suite itself are to be skipped. 112566201aeSJuraj Linkeš 113566201aeSJuraj Linkeš Returns: 114566201aeSJuraj Linkeš :data:`True` if the test suite should be skipped, :data:`False` otherwise. 115566201aeSJuraj Linkeš """ 116566201aeSJuraj Linkeš return all(test_case.skip for test_case in self.test_cases) or self.test_suite_class.skip 117566201aeSJuraj Linkeš 1180c6f2d11SJuraj Linkeš 1190c6f2d11SJuraj Linkešclass Result(Enum): 1206ef07151SJuraj Linkeš """The possible states that a setup, a teardown or a test case may end up in.""" 1210c6f2d11SJuraj Linkeš 1226ef07151SJuraj Linkeš #: 1230c6f2d11SJuraj Linkeš PASS = auto() 1246ef07151SJuraj Linkeš #: 1250c6f2d11SJuraj Linkeš FAIL = auto() 1266ef07151SJuraj Linkeš #: 1270c6f2d11SJuraj Linkeš ERROR = auto() 1286ef07151SJuraj Linkeš #: 129caae1889SJuraj Linkeš BLOCK = auto() 130566201aeSJuraj Linkeš #: 131566201aeSJuraj Linkeš SKIP = auto() 1320c6f2d11SJuraj Linkeš 1330c6f2d11SJuraj Linkeš def __bool__(self) -> bool: 134566201aeSJuraj Linkeš """Only :attr:`PASS` is True.""" 1350c6f2d11SJuraj Linkeš return self is self.PASS 1360c6f2d11SJuraj Linkeš 1370c6f2d11SJuraj Linkeš 138*9f8a2572STomáš Ďurovecclass TestCaseResultDict(TypedDict): 139*9f8a2572STomáš Ďurovec """Represents the `TestCaseResult` results. 140*9f8a2572STomáš Ďurovec 141*9f8a2572STomáš Ďurovec Attributes: 142*9f8a2572STomáš Ďurovec test_case_name: The name of the test case. 143*9f8a2572STomáš Ďurovec result: The result name of the test case. 144*9f8a2572STomáš Ďurovec """ 145*9f8a2572STomáš Ďurovec 146*9f8a2572STomáš Ďurovec test_case_name: str 147*9f8a2572STomáš Ďurovec result: str 148*9f8a2572STomáš Ďurovec 149*9f8a2572STomáš Ďurovec 150*9f8a2572STomáš Ďurovecclass TestSuiteResultDict(TypedDict): 151*9f8a2572STomáš Ďurovec """Represents the `TestSuiteResult` results. 152*9f8a2572STomáš Ďurovec 153*9f8a2572STomáš Ďurovec Attributes: 154*9f8a2572STomáš Ďurovec test_suite_name: The name of the test suite. 155*9f8a2572STomáš Ďurovec test_cases: A list of test case results contained in this test suite. 156*9f8a2572STomáš Ďurovec """ 157*9f8a2572STomáš Ďurovec 158*9f8a2572STomáš Ďurovec test_suite_name: str 159*9f8a2572STomáš Ďurovec test_cases: list[TestCaseResultDict] 160*9f8a2572STomáš Ďurovec 161*9f8a2572STomáš Ďurovec 162*9f8a2572STomáš Ďurovecclass TestRunResultDict(TypedDict, total=False): 163*9f8a2572STomáš Ďurovec """Represents the `TestRunResult` results. 164*9f8a2572STomáš Ďurovec 165*9f8a2572STomáš Ďurovec Attributes: 166*9f8a2572STomáš Ďurovec compiler_version: The version of the compiler used for the DPDK build. 167*9f8a2572STomáš Ďurovec dpdk_version: The version of DPDK being tested. 168*9f8a2572STomáš Ďurovec ports: A list of ports associated with the test run. 169*9f8a2572STomáš Ďurovec test_suites: A list of test suite results included in this test run. 170*9f8a2572STomáš Ďurovec summary: A dictionary containing overall results, such as pass/fail counts. 171*9f8a2572STomáš Ďurovec """ 172*9f8a2572STomáš Ďurovec 173*9f8a2572STomáš Ďurovec compiler_version: str | None 174*9f8a2572STomáš Ďurovec dpdk_version: str | None 175*9f8a2572STomáš Ďurovec ports: list[dict[str, Any]] 176*9f8a2572STomáš Ďurovec test_suites: list[TestSuiteResultDict] 177*9f8a2572STomáš Ďurovec summary: dict[str, int | float] 178*9f8a2572STomáš Ďurovec 179*9f8a2572STomáš Ďurovec 180*9f8a2572STomáš Ďurovecclass DtsRunResultDict(TypedDict): 181*9f8a2572STomáš Ďurovec """Represents the `DtsRunResult` results. 182*9f8a2572STomáš Ďurovec 183*9f8a2572STomáš Ďurovec Attributes: 184*9f8a2572STomáš Ďurovec test_runs: A list of test run results. 185*9f8a2572STomáš Ďurovec summary: A summary dictionary containing overall statistics for the test runs. 186*9f8a2572STomáš Ďurovec """ 187*9f8a2572STomáš Ďurovec 188*9f8a2572STomáš Ďurovec test_runs: list[TestRunResultDict] 189*9f8a2572STomáš Ďurovec summary: dict[str, int | float] 190*9f8a2572STomáš Ďurovec 191*9f8a2572STomáš Ďurovec 1923e967643SJuraj Linkešclass FixtureResult: 1936ef07151SJuraj Linkeš """A record that stores the result of a setup or a teardown. 1946ef07151SJuraj Linkeš 1956ef07151SJuraj Linkeš :attr:`~Result.FAIL` is a sensible default since it prevents false positives (which could happen 1966ef07151SJuraj Linkeš if the default was :attr:`~Result.PASS`). 1976ef07151SJuraj Linkeš 1986ef07151SJuraj Linkeš Preventing false positives or other false results is preferable since a failure 1996ef07151SJuraj Linkeš is mostly likely to be investigated (the other false results may not be investigated at all). 2006ef07151SJuraj Linkeš 2016ef07151SJuraj Linkeš Attributes: 2026ef07151SJuraj Linkeš result: The associated result. 2036ef07151SJuraj Linkeš error: The error in case of a failure. 2040c6f2d11SJuraj Linkeš """ 2050c6f2d11SJuraj Linkeš 2060c6f2d11SJuraj Linkeš result: Result 2070c6f2d11SJuraj Linkeš error: Exception | None = None 2080c6f2d11SJuraj Linkeš 2090c6f2d11SJuraj Linkeš def __init__( 2100c6f2d11SJuraj Linkeš self, 2110c6f2d11SJuraj Linkeš result: Result = Result.FAIL, 2120c6f2d11SJuraj Linkeš error: Exception | None = None, 2130c6f2d11SJuraj Linkeš ): 2146ef07151SJuraj Linkeš """Initialize the constructor with the fixture result and store a possible error. 2156ef07151SJuraj Linkeš 2166ef07151SJuraj Linkeš Args: 2176ef07151SJuraj Linkeš result: The result to store. 2186ef07151SJuraj Linkeš error: The error which happened when a failure occurred. 2196ef07151SJuraj Linkeš """ 2200c6f2d11SJuraj Linkeš self.result = result 2210c6f2d11SJuraj Linkeš self.error = error 2220c6f2d11SJuraj Linkeš 2230c6f2d11SJuraj Linkeš def __bool__(self) -> bool: 2246ef07151SJuraj Linkeš """A wrapper around the stored :class:`Result`.""" 2250c6f2d11SJuraj Linkeš return bool(self.result) 2260c6f2d11SJuraj Linkeš 2270c6f2d11SJuraj Linkeš 2283e967643SJuraj Linkešclass BaseResult: 2296ef07151SJuraj Linkeš """Common data and behavior of DTS results. 2306ef07151SJuraj Linkeš 2316ef07151SJuraj Linkeš Stores the results of the setup and teardown portions of the corresponding stage. 2326ef07151SJuraj Linkeš The hierarchical nature of DTS results is captured recursively in an internal list. 23385ceeeceSJuraj Linkeš A stage is each level in this particular hierarchy (pre-run or the top-most level, 23411b2279aSTomáš Ďurovec test run, test suite and test case). 2356ef07151SJuraj Linkeš 2366ef07151SJuraj Linkeš Attributes: 2376ef07151SJuraj Linkeš setup_result: The result of the setup of the particular stage. 2386ef07151SJuraj Linkeš teardown_result: The results of the teardown of the particular stage. 239caae1889SJuraj Linkeš child_results: The results of the descendants in the results hierarchy. 2400c6f2d11SJuraj Linkeš """ 2410c6f2d11SJuraj Linkeš 2420c6f2d11SJuraj Linkeš setup_result: FixtureResult 2430c6f2d11SJuraj Linkeš teardown_result: FixtureResult 244caae1889SJuraj Linkeš child_results: MutableSequence["BaseResult"] 2450c6f2d11SJuraj Linkeš 2460c6f2d11SJuraj Linkeš def __init__(self): 2476ef07151SJuraj Linkeš """Initialize the constructor.""" 2480c6f2d11SJuraj Linkeš self.setup_result = FixtureResult() 2490c6f2d11SJuraj Linkeš self.teardown_result = FixtureResult() 250caae1889SJuraj Linkeš self.child_results = [] 2510c6f2d11SJuraj Linkeš 2520c6f2d11SJuraj Linkeš def update_setup(self, result: Result, error: Exception | None = None) -> None: 2536ef07151SJuraj Linkeš """Store the setup result. 2546ef07151SJuraj Linkeš 255caae1889SJuraj Linkeš If the result is :attr:`~Result.BLOCK`, :attr:`~Result.ERROR` or :attr:`~Result.FAIL`, 256caae1889SJuraj Linkeš then the corresponding child results in result hierarchy 257caae1889SJuraj Linkeš are also marked with :attr:`~Result.BLOCK`. 258caae1889SJuraj Linkeš 2596ef07151SJuraj Linkeš Args: 2606ef07151SJuraj Linkeš result: The result of the setup. 2616ef07151SJuraj Linkeš error: The error that occurred in case of a failure. 2626ef07151SJuraj Linkeš """ 2630c6f2d11SJuraj Linkeš self.setup_result.result = result 2640c6f2d11SJuraj Linkeš self.setup_result.error = error 2650c6f2d11SJuraj Linkeš 266566201aeSJuraj Linkeš if result != Result.PASS: 267566201aeSJuraj Linkeš result_to_mark = Result.BLOCK if result != Result.SKIP else Result.SKIP 268566201aeSJuraj Linkeš self.update_teardown(result_to_mark) 269566201aeSJuraj Linkeš self._mark_results(result_to_mark) 270caae1889SJuraj Linkeš 271566201aeSJuraj Linkeš def _mark_results(self, result) -> None: 272566201aeSJuraj Linkeš """Mark the child results or the result of the level itself as `result`. 273caae1889SJuraj Linkeš 274566201aeSJuraj Linkeš The marking of results should be done in overloaded methods. 275caae1889SJuraj Linkeš """ 276caae1889SJuraj Linkeš 2770c6f2d11SJuraj Linkeš def update_teardown(self, result: Result, error: Exception | None = None) -> None: 2786ef07151SJuraj Linkeš """Store the teardown result. 2796ef07151SJuraj Linkeš 2806ef07151SJuraj Linkeš Args: 2816ef07151SJuraj Linkeš result: The result of the teardown. 2826ef07151SJuraj Linkeš error: The error that occurred in case of a failure. 2836ef07151SJuraj Linkeš """ 2840c6f2d11SJuraj Linkeš self.teardown_result.result = result 2850c6f2d11SJuraj Linkeš self.teardown_result.error = error 2860c6f2d11SJuraj Linkeš 2870c6f2d11SJuraj Linkeš def _get_setup_teardown_errors(self) -> list[Exception]: 2880c6f2d11SJuraj Linkeš errors = [] 2890c6f2d11SJuraj Linkeš if self.setup_result.error: 2900c6f2d11SJuraj Linkeš errors.append(self.setup_result.error) 2910c6f2d11SJuraj Linkeš if self.teardown_result.error: 2920c6f2d11SJuraj Linkeš errors.append(self.teardown_result.error) 2930c6f2d11SJuraj Linkeš return errors 2940c6f2d11SJuraj Linkeš 295caae1889SJuraj Linkeš def _get_child_errors(self) -> list[Exception]: 296caae1889SJuraj Linkeš return [error for child_result in self.child_results for error in child_result.get_errors()] 2970c6f2d11SJuraj Linkeš 2980c6f2d11SJuraj Linkeš def get_errors(self) -> list[Exception]: 2996ef07151SJuraj Linkeš """Compile errors from the whole result hierarchy. 3006ef07151SJuraj Linkeš 3016ef07151SJuraj Linkeš Returns: 3026ef07151SJuraj Linkeš The errors from setup, teardown and all errors found in the whole result hierarchy. 3036ef07151SJuraj Linkeš """ 304caae1889SJuraj Linkeš return self._get_setup_teardown_errors() + self._get_child_errors() 3050c6f2d11SJuraj Linkeš 306*9f8a2572STomáš Ďurovec def to_dict(self): 307*9f8a2572STomáš Ďurovec """Convert the results hierarchy into a dictionary representation.""" 308*9f8a2572STomáš Ďurovec 309*9f8a2572STomáš Ďurovec def add_result(self, results: dict[str, int]): 310*9f8a2572STomáš Ďurovec """Collate the test case result to the given result hierarchy. 3116ef07151SJuraj Linkeš 3126ef07151SJuraj Linkeš Args: 313*9f8a2572STomáš Ďurovec results: The dictionary in which results will be collated. 3146ef07151SJuraj Linkeš """ 315caae1889SJuraj Linkeš for child_result in self.child_results: 316*9f8a2572STomáš Ďurovec child_result.add_result(results) 317*9f8a2572STomáš Ďurovec 318*9f8a2572STomáš Ďurovec def generate_pass_rate_dict(self, test_run_summary) -> dict[str, float]: 319*9f8a2572STomáš Ďurovec """Generate a dictionary with the PASS/FAIL ratio of all test cases. 320*9f8a2572STomáš Ďurovec 321*9f8a2572STomáš Ďurovec Args: 322*9f8a2572STomáš Ďurovec test_run_summary: The summary dictionary containing test result counts. 323*9f8a2572STomáš Ďurovec 324*9f8a2572STomáš Ďurovec Returns: 325*9f8a2572STomáš Ďurovec A dictionary with the PASS/FAIL ratio of all test cases. 326*9f8a2572STomáš Ďurovec """ 327*9f8a2572STomáš Ďurovec return { 328*9f8a2572STomáš Ďurovec "PASS_RATE": ( 329*9f8a2572STomáš Ďurovec float(test_run_summary[Result.PASS.name]) 330*9f8a2572STomáš Ďurovec * 100 331*9f8a2572STomáš Ďurovec / sum(test_run_summary[result.name] for result in Result if result != Result.SKIP) 332*9f8a2572STomáš Ďurovec ) 333*9f8a2572STomáš Ďurovec } 3340c6f2d11SJuraj Linkeš 3350c6f2d11SJuraj Linkeš 3360c6f2d11SJuraj Linkešclass DTSResult(BaseResult): 3376ef07151SJuraj Linkeš """Stores environment information and test results from a DTS run. 3386ef07151SJuraj Linkeš 33911b2279aSTomáš Ďurovec * Test run level information, such as testbed, the test suite list and 34011b2279aSTomáš Ďurovec DPDK build configuration (compiler, target OS and cpu), 3416ef07151SJuraj Linkeš * Test suite and test case results, 3420c6f2d11SJuraj Linkeš * All errors that are caught and recorded during DTS execution. 3430c6f2d11SJuraj Linkeš 3446ef07151SJuraj Linkeš The information is stored hierarchically. This is the first level of the hierarchy 3456ef07151SJuraj Linkeš and as such is where the data form the whole hierarchy is collated or processed. 3460c6f2d11SJuraj Linkeš 34785ceeeceSJuraj Linkeš The internal list stores the results of all test runs. 3480c6f2d11SJuraj Linkeš """ 3490c6f2d11SJuraj Linkeš 350*9f8a2572STomáš Ďurovec _output_dir: str 35104f5a5a6SJuraj Linkeš _logger: DTSLogger 3520c6f2d11SJuraj Linkeš _errors: list[Exception] 3530c6f2d11SJuraj Linkeš _return_code: ErrorSeverity 3540c6f2d11SJuraj Linkeš 355*9f8a2572STomáš Ďurovec def __init__(self, output_dir: str, logger: DTSLogger): 3566ef07151SJuraj Linkeš """Extend the constructor with top-level specifics. 3576ef07151SJuraj Linkeš 3586ef07151SJuraj Linkeš Args: 359*9f8a2572STomáš Ďurovec output_dir: The directory where DTS logs and results are saved. 3606ef07151SJuraj Linkeš logger: The logger instance the whole result will use. 3616ef07151SJuraj Linkeš """ 362f614e737SJuraj Linkeš super().__init__() 363*9f8a2572STomáš Ďurovec self._output_dir = output_dir 3640c6f2d11SJuraj Linkeš self._logger = logger 3650c6f2d11SJuraj Linkeš self._errors = [] 3660c6f2d11SJuraj Linkeš self._return_code = ErrorSeverity.NO_ERR 3670c6f2d11SJuraj Linkeš 36885ceeeceSJuraj Linkeš def add_test_run(self, test_run_config: TestRunConfiguration) -> "TestRunResult": 36985ceeeceSJuraj Linkeš """Add and return the child result (test run). 3706ef07151SJuraj Linkeš 3716ef07151SJuraj Linkeš Args: 37285ceeeceSJuraj Linkeš test_run_config: A test run configuration. 3736ef07151SJuraj Linkeš 3746ef07151SJuraj Linkeš Returns: 37585ceeeceSJuraj Linkeš The test run's result. 3766ef07151SJuraj Linkeš """ 37785ceeeceSJuraj Linkeš result = TestRunResult(test_run_config) 378caae1889SJuraj Linkeš self.child_results.append(result) 379caae1889SJuraj Linkeš return result 3800c6f2d11SJuraj Linkeš 381840b1e01SJuraj Linkeš def add_error(self, error: Exception) -> None: 38285ceeeceSJuraj Linkeš """Record an error that occurred outside any test run. 3836ef07151SJuraj Linkeš 3846ef07151SJuraj Linkeš Args: 3856ef07151SJuraj Linkeš error: The exception to record. 3866ef07151SJuraj Linkeš """ 3870c6f2d11SJuraj Linkeš self._errors.append(error) 3880c6f2d11SJuraj Linkeš 3890c6f2d11SJuraj Linkeš def process(self) -> None: 3906ef07151SJuraj Linkeš """Process the data after a whole DTS run. 3910c6f2d11SJuraj Linkeš 392caae1889SJuraj Linkeš The data is added to child objects during runtime and this object is not updated 393caae1889SJuraj Linkeš at that time. This requires us to process the child data after it's all been gathered. 3946ef07151SJuraj Linkeš 3956ef07151SJuraj Linkeš The processing gathers all errors and the statistics of test case results. 3960c6f2d11SJuraj Linkeš """ 3970c6f2d11SJuraj Linkeš self._errors += self.get_errors() 3980c6f2d11SJuraj Linkeš if self._errors and self._logger: 3990c6f2d11SJuraj Linkeš self._logger.debug("Summary of errors:") 4000c6f2d11SJuraj Linkeš for error in self._errors: 4010c6f2d11SJuraj Linkeš self._logger.debug(repr(error)) 4020c6f2d11SJuraj Linkeš 403*9f8a2572STomáš Ďurovec TextSummary(self).save(Path(self._output_dir, "results_summary.txt")) 404*9f8a2572STomáš Ďurovec JsonResults(self).save(Path(self._output_dir, "results.json")) 4050c6f2d11SJuraj Linkeš 4060c6f2d11SJuraj Linkeš def get_return_code(self) -> int: 4076ef07151SJuraj Linkeš """Go through all stored Exceptions and return the final DTS error code. 4086ef07151SJuraj Linkeš 4096ef07151SJuraj Linkeš Returns: 4106ef07151SJuraj Linkeš The highest error code found. 4110c6f2d11SJuraj Linkeš """ 4120c6f2d11SJuraj Linkeš for error in self._errors: 4130c6f2d11SJuraj Linkeš error_return_code = ErrorSeverity.GENERIC_ERR 4140c6f2d11SJuraj Linkeš if isinstance(error, DTSError): 4150c6f2d11SJuraj Linkeš error_return_code = error.severity 4160c6f2d11SJuraj Linkeš 4170c6f2d11SJuraj Linkeš if error_return_code > self._return_code: 4180c6f2d11SJuraj Linkeš self._return_code = error_return_code 4190c6f2d11SJuraj Linkeš 4200c6f2d11SJuraj Linkeš return int(self._return_code) 421e1f61839SJuraj Linkeš 422*9f8a2572STomáš Ďurovec def to_dict(self) -> DtsRunResultDict: 423*9f8a2572STomáš Ďurovec """Convert DTS result into a dictionary format. 424*9f8a2572STomáš Ďurovec 425*9f8a2572STomáš Ďurovec The dictionary contains test runs and summary of test runs. 426*9f8a2572STomáš Ďurovec 427*9f8a2572STomáš Ďurovec Returns: 428*9f8a2572STomáš Ďurovec A dictionary representation of the DTS result 429*9f8a2572STomáš Ďurovec """ 430*9f8a2572STomáš Ďurovec 431*9f8a2572STomáš Ďurovec def merge_test_run_summaries(test_run_summaries: list[dict[str, int]]) -> dict[str, int]: 432*9f8a2572STomáš Ďurovec """Merge multiple test run summaries into one dictionary. 433*9f8a2572STomáš Ďurovec 434*9f8a2572STomáš Ďurovec Args: 435*9f8a2572STomáš Ďurovec test_run_summaries: List of test run summary dictionaries. 436*9f8a2572STomáš Ďurovec 437*9f8a2572STomáš Ďurovec Returns: 438*9f8a2572STomáš Ďurovec A merged dictionary containing the aggregated summary. 439*9f8a2572STomáš Ďurovec """ 440*9f8a2572STomáš Ďurovec return { 441*9f8a2572STomáš Ďurovec key.name: sum(test_run_summary[key.name] for test_run_summary in test_run_summaries) 442*9f8a2572STomáš Ďurovec for key in Result 443*9f8a2572STomáš Ďurovec } 444*9f8a2572STomáš Ďurovec 445*9f8a2572STomáš Ďurovec test_runs = [child.to_dict() for child in self.child_results] 446*9f8a2572STomáš Ďurovec test_run_summary = merge_test_run_summaries([test_run["summary"] for test_run in test_runs]) 447*9f8a2572STomáš Ďurovec 448*9f8a2572STomáš Ďurovec return { 449*9f8a2572STomáš Ďurovec "test_runs": test_runs, 450*9f8a2572STomáš Ďurovec "summary": test_run_summary | self.generate_pass_rate_dict(test_run_summary), 451*9f8a2572STomáš Ďurovec } 452*9f8a2572STomáš Ďurovec 453e1f61839SJuraj Linkeš 45485ceeeceSJuraj Linkešclass TestRunResult(BaseResult): 45585ceeeceSJuraj Linkeš """The test run specific result. 456e1f61839SJuraj Linkeš 45711b2279aSTomáš Ďurovec The internal list stores the results of all test suites in a given test run. 458e1f61839SJuraj Linkeš 459e1f61839SJuraj Linkeš Attributes: 46011b2279aSTomáš Ďurovec compiler_version: The DPDK build compiler version. 46111b2279aSTomáš Ďurovec dpdk_version: The built DPDK version. 462e1f61839SJuraj Linkeš sut_os_name: The operating system of the SUT node. 463e1f61839SJuraj Linkeš sut_os_version: The operating system version of the SUT node. 464e1f61839SJuraj Linkeš sut_kernel_version: The operating system kernel version of the SUT node. 465e1f61839SJuraj Linkeš """ 466e1f61839SJuraj Linkeš 46785ceeeceSJuraj Linkeš _config: TestRunConfiguration 468caae1889SJuraj Linkeš _test_suites_with_cases: list[TestSuiteWithCases] 469*9f8a2572STomáš Ďurovec _ports: list[Port] 470*9f8a2572STomáš Ďurovec _sut_info: OSSessionInfo | None 471*9f8a2572STomáš Ďurovec _dpdk_build_info: DPDKBuildInfo | None 472e1f61839SJuraj Linkeš 47385ceeeceSJuraj Linkeš def __init__(self, test_run_config: TestRunConfiguration): 47411b2279aSTomáš Ďurovec """Extend the constructor with the test run's config. 475e1f61839SJuraj Linkeš 476e1f61839SJuraj Linkeš Args: 47785ceeeceSJuraj Linkeš test_run_config: A test run configuration. 478e1f61839SJuraj Linkeš """ 479f614e737SJuraj Linkeš super().__init__() 48085ceeeceSJuraj Linkeš self._config = test_run_config 481caae1889SJuraj Linkeš self._test_suites_with_cases = [] 482*9f8a2572STomáš Ďurovec self._ports = [] 483*9f8a2572STomáš Ďurovec self._sut_info = None 484*9f8a2572STomáš Ďurovec self._dpdk_build_info = None 485e1f61839SJuraj Linkeš 48611b2279aSTomáš Ďurovec def add_test_suite( 48711b2279aSTomáš Ďurovec self, 48811b2279aSTomáš Ďurovec test_suite_with_cases: TestSuiteWithCases, 48911b2279aSTomáš Ďurovec ) -> "TestSuiteResult": 49011b2279aSTomáš Ďurovec """Add and return the child result (test suite). 491e1f61839SJuraj Linkeš 492e1f61839SJuraj Linkeš Args: 49311b2279aSTomáš Ďurovec test_suite_with_cases: The test suite with test cases. 494e1f61839SJuraj Linkeš 495e1f61839SJuraj Linkeš Returns: 49611b2279aSTomáš Ďurovec The test suite's result. 497e1f61839SJuraj Linkeš """ 49811b2279aSTomáš Ďurovec result = TestSuiteResult(test_suite_with_cases) 499caae1889SJuraj Linkeš self.child_results.append(result) 500caae1889SJuraj Linkeš return result 501caae1889SJuraj Linkeš 502caae1889SJuraj Linkeš @property 503caae1889SJuraj Linkeš def test_suites_with_cases(self) -> list[TestSuiteWithCases]: 50485ceeeceSJuraj Linkeš """The test suites with test cases to be executed in this test run. 505caae1889SJuraj Linkeš 506caae1889SJuraj Linkeš The test suites can only be assigned once. 507caae1889SJuraj Linkeš 508caae1889SJuraj Linkeš Returns: 509caae1889SJuraj Linkeš The list of test suites with test cases. If an error occurs between 51085ceeeceSJuraj Linkeš the initialization of :class:`TestRunResult` and assigning test cases to the instance, 511caae1889SJuraj Linkeš return an empty list, representing that we don't know what to execute. 512caae1889SJuraj Linkeš """ 513caae1889SJuraj Linkeš return self._test_suites_with_cases 514caae1889SJuraj Linkeš 515caae1889SJuraj Linkeš @test_suites_with_cases.setter 516caae1889SJuraj Linkeš def test_suites_with_cases(self, test_suites_with_cases: list[TestSuiteWithCases]) -> None: 517caae1889SJuraj Linkeš if self._test_suites_with_cases: 518caae1889SJuraj Linkeš raise ValueError( 51985ceeeceSJuraj Linkeš "Attempted to assign test suites to a test run result " 520caae1889SJuraj Linkeš "which already has test suites." 521caae1889SJuraj Linkeš ) 522caae1889SJuraj Linkeš self._test_suites_with_cases = test_suites_with_cases 523e1f61839SJuraj Linkeš 524*9f8a2572STomáš Ďurovec @property 525*9f8a2572STomáš Ďurovec def ports(self) -> list[Port]: 526*9f8a2572STomáš Ďurovec """Get the list of ports associated with this test run.""" 527*9f8a2572STomáš Ďurovec return self._ports 528*9f8a2572STomáš Ďurovec 529*9f8a2572STomáš Ďurovec @ports.setter 530*9f8a2572STomáš Ďurovec def ports(self, ports: list[Port]) -> None: 531*9f8a2572STomáš Ďurovec """Set the list of ports associated with this test run. 532e1f61839SJuraj Linkeš 533e1f61839SJuraj Linkeš Args: 534*9f8a2572STomáš Ďurovec ports: The list of ports to associate with this test run. 535e1f61839SJuraj Linkeš 536*9f8a2572STomáš Ďurovec Raises: 537*9f8a2572STomáš Ďurovec ValueError: If the ports have already been assigned to this test run. 538*9f8a2572STomáš Ďurovec """ 539*9f8a2572STomáš Ďurovec if self._ports: 540*9f8a2572STomáš Ďurovec raise ValueError( 541*9f8a2572STomáš Ďurovec "Attempted to assign `ports` to a test run result which already has `ports`." 542*9f8a2572STomáš Ďurovec ) 543*9f8a2572STomáš Ďurovec self._ports = ports 544*9f8a2572STomáš Ďurovec 545*9f8a2572STomáš Ďurovec @property 546*9f8a2572STomáš Ďurovec def sut_info(self) -> OSSessionInfo | None: 547*9f8a2572STomáš Ďurovec """Get the SUT OS session information associated with this test run.""" 548*9f8a2572STomáš Ďurovec return self._sut_info 549*9f8a2572STomáš Ďurovec 550*9f8a2572STomáš Ďurovec @sut_info.setter 551*9f8a2572STomáš Ďurovec def sut_info(self, sut_info: OSSessionInfo) -> None: 552*9f8a2572STomáš Ďurovec """Set the SUT node information associated with this test run. 553e1f61839SJuraj Linkeš 554e1f61839SJuraj Linkeš Args: 555*9f8a2572STomáš Ďurovec sut_info: The SUT node information to associate with this test run. 556*9f8a2572STomáš Ďurovec 557*9f8a2572STomáš Ďurovec Raises: 558*9f8a2572STomáš Ďurovec ValueError: If the SUT information has already been assigned to this test run. 559e1f61839SJuraj Linkeš """ 560*9f8a2572STomáš Ďurovec if self._sut_info: 561*9f8a2572STomáš Ďurovec raise ValueError( 562*9f8a2572STomáš Ďurovec "Attempted to assign `sut_info` to a test run result which already has `sut_info`." 563*9f8a2572STomáš Ďurovec ) 564*9f8a2572STomáš Ďurovec self._sut_info = sut_info 565*9f8a2572STomáš Ďurovec 566*9f8a2572STomáš Ďurovec @property 567*9f8a2572STomáš Ďurovec def dpdk_build_info(self) -> DPDKBuildInfo | None: 568*9f8a2572STomáš Ďurovec """Get the DPDK build information associated with this test run.""" 569*9f8a2572STomáš Ďurovec return self._dpdk_build_info 570*9f8a2572STomáš Ďurovec 571*9f8a2572STomáš Ďurovec @dpdk_build_info.setter 572*9f8a2572STomáš Ďurovec def dpdk_build_info(self, dpdk_build_info: DPDKBuildInfo) -> None: 573*9f8a2572STomáš Ďurovec """Set the DPDK build information associated with this test run. 574*9f8a2572STomáš Ďurovec 575*9f8a2572STomáš Ďurovec Args: 576*9f8a2572STomáš Ďurovec dpdk_build_info: The DPDK build information to associate with this test run. 577*9f8a2572STomáš Ďurovec 578*9f8a2572STomáš Ďurovec Raises: 579*9f8a2572STomáš Ďurovec ValueError: If the DPDK build information has already been assigned to this test run. 580*9f8a2572STomáš Ďurovec """ 581*9f8a2572STomáš Ďurovec if self._dpdk_build_info: 582*9f8a2572STomáš Ďurovec raise ValueError( 583*9f8a2572STomáš Ďurovec "Attempted to assign `dpdk_build_info` to a test run result which already " 584*9f8a2572STomáš Ďurovec "has `dpdk_build_info`." 585*9f8a2572STomáš Ďurovec ) 586*9f8a2572STomáš Ďurovec self._dpdk_build_info = dpdk_build_info 587*9f8a2572STomáš Ďurovec 588*9f8a2572STomáš Ďurovec def to_dict(self) -> TestRunResultDict: 589*9f8a2572STomáš Ďurovec """Convert the test run result into a dictionary. 590*9f8a2572STomáš Ďurovec 591*9f8a2572STomáš Ďurovec The dictionary contains test suites in this test run, and a summary of the test run and 592*9f8a2572STomáš Ďurovec information about the DPDK version, compiler version and associated ports. 593*9f8a2572STomáš Ďurovec 594*9f8a2572STomáš Ďurovec Returns: 595*9f8a2572STomáš Ďurovec TestRunResultDict: A dictionary representation of the test run result. 596*9f8a2572STomáš Ďurovec """ 597*9f8a2572STomáš Ďurovec results = {result.name: 0 for result in Result} 598*9f8a2572STomáš Ďurovec self.add_result(results) 599*9f8a2572STomáš Ďurovec 600*9f8a2572STomáš Ďurovec compiler_version = None 601*9f8a2572STomáš Ďurovec dpdk_version = None 602*9f8a2572STomáš Ďurovec 603*9f8a2572STomáš Ďurovec if self.dpdk_build_info: 604*9f8a2572STomáš Ďurovec compiler_version = self.dpdk_build_info.compiler_version 605*9f8a2572STomáš Ďurovec dpdk_version = self.dpdk_build_info.dpdk_version 606*9f8a2572STomáš Ďurovec 607*9f8a2572STomáš Ďurovec return { 608*9f8a2572STomáš Ďurovec "compiler_version": compiler_version, 609*9f8a2572STomáš Ďurovec "dpdk_version": dpdk_version, 610*9f8a2572STomáš Ďurovec "ports": [asdict(port) for port in self.ports], 611*9f8a2572STomáš Ďurovec "test_suites": [child.to_dict() for child in self.child_results], 612*9f8a2572STomáš Ďurovec "summary": results | self.generate_pass_rate_dict(results), 613*9f8a2572STomáš Ďurovec } 614e1f61839SJuraj Linkeš 615566201aeSJuraj Linkeš def _mark_results(self, result) -> None: 616566201aeSJuraj Linkeš """Mark the test suite results as `result`.""" 617caae1889SJuraj Linkeš for test_suite_with_cases in self._test_suites_with_cases: 618caae1889SJuraj Linkeš child_result = self.add_test_suite(test_suite_with_cases) 619566201aeSJuraj Linkeš child_result.update_setup(result) 620e1f61839SJuraj Linkeš 621e1f61839SJuraj Linkeš 622e1f61839SJuraj Linkešclass TestSuiteResult(BaseResult): 623e1f61839SJuraj Linkeš """The test suite specific result. 624e1f61839SJuraj Linkeš 625e1f61839SJuraj Linkeš The internal list stores the results of all test cases in a given test suite. 626e1f61839SJuraj Linkeš 627e1f61839SJuraj Linkeš Attributes: 628caae1889SJuraj Linkeš test_suite_name: The test suite name. 629e1f61839SJuraj Linkeš """ 630e1f61839SJuraj Linkeš 631caae1889SJuraj Linkeš test_suite_name: str 632caae1889SJuraj Linkeš _test_suite_with_cases: TestSuiteWithCases 633caae1889SJuraj Linkeš _child_configs: list[str] 634e1f61839SJuraj Linkeš 635caae1889SJuraj Linkeš def __init__(self, test_suite_with_cases: TestSuiteWithCases): 63611b2279aSTomáš Ďurovec """Extend the constructor with test suite's config. 637e1f61839SJuraj Linkeš 638e1f61839SJuraj Linkeš Args: 639caae1889SJuraj Linkeš test_suite_with_cases: The test suite with test cases. 640e1f61839SJuraj Linkeš """ 641f614e737SJuraj Linkeš super().__init__() 642caae1889SJuraj Linkeš self.test_suite_name = test_suite_with_cases.test_suite_class.__name__ 643caae1889SJuraj Linkeš self._test_suite_with_cases = test_suite_with_cases 644e1f61839SJuraj Linkeš 645e1f61839SJuraj Linkeš def add_test_case(self, test_case_name: str) -> "TestCaseResult": 646caae1889SJuraj Linkeš """Add and return the child result (test case). 647caae1889SJuraj Linkeš 648caae1889SJuraj Linkeš Args: 649caae1889SJuraj Linkeš test_case_name: The name of the test case. 650e1f61839SJuraj Linkeš 651e1f61839SJuraj Linkeš Returns: 652e1f61839SJuraj Linkeš The test case's result. 653e1f61839SJuraj Linkeš """ 654caae1889SJuraj Linkeš result = TestCaseResult(test_case_name) 655caae1889SJuraj Linkeš self.child_results.append(result) 656caae1889SJuraj Linkeš return result 657caae1889SJuraj Linkeš 658*9f8a2572STomáš Ďurovec def to_dict(self) -> TestSuiteResultDict: 659*9f8a2572STomáš Ďurovec """Convert the test suite result into a dictionary. 660*9f8a2572STomáš Ďurovec 661*9f8a2572STomáš Ďurovec The dictionary contains a test suite name and test cases given in this test suite. 662*9f8a2572STomáš Ďurovec """ 663*9f8a2572STomáš Ďurovec return { 664*9f8a2572STomáš Ďurovec "test_suite_name": self.test_suite_name, 665*9f8a2572STomáš Ďurovec "test_cases": [child.to_dict() for child in self.child_results], 666*9f8a2572STomáš Ďurovec } 667*9f8a2572STomáš Ďurovec 668566201aeSJuraj Linkeš def _mark_results(self, result) -> None: 669566201aeSJuraj Linkeš """Mark the test case results as `result`.""" 670caae1889SJuraj Linkeš for test_case_method in self._test_suite_with_cases.test_cases: 671caae1889SJuraj Linkeš child_result = self.add_test_case(test_case_method.__name__) 672566201aeSJuraj Linkeš child_result.update_setup(result) 673e1f61839SJuraj Linkeš 674e1f61839SJuraj Linkeš 675e1f61839SJuraj Linkešclass TestCaseResult(BaseResult, FixtureResult): 676e1f61839SJuraj Linkeš r"""The test case specific result. 677e1f61839SJuraj Linkeš 678e1f61839SJuraj Linkeš Stores the result of the actual test case. This is done by adding an extra superclass 679e1f61839SJuraj Linkeš in :class:`FixtureResult`. The setup and teardown results are :class:`FixtureResult`\s and 680e1f61839SJuraj Linkeš the class is itself a record of the test case. 681e1f61839SJuraj Linkeš 682e1f61839SJuraj Linkeš Attributes: 683e1f61839SJuraj Linkeš test_case_name: The test case name. 684e1f61839SJuraj Linkeš """ 685e1f61839SJuraj Linkeš 686e1f61839SJuraj Linkeš test_case_name: str 687e1f61839SJuraj Linkeš 688e1f61839SJuraj Linkeš def __init__(self, test_case_name: str): 68911b2279aSTomáš Ďurovec """Extend the constructor with test case's name. 690e1f61839SJuraj Linkeš 691e1f61839SJuraj Linkeš Args: 692e1f61839SJuraj Linkeš test_case_name: The test case's name. 693e1f61839SJuraj Linkeš """ 694f614e737SJuraj Linkeš super().__init__() 695e1f61839SJuraj Linkeš self.test_case_name = test_case_name 696e1f61839SJuraj Linkeš 697e1f61839SJuraj Linkeš def update(self, result: Result, error: Exception | None = None) -> None: 698e1f61839SJuraj Linkeš """Update the test case result. 699e1f61839SJuraj Linkeš 700e1f61839SJuraj Linkeš This updates the result of the test case itself and doesn't affect 701e1f61839SJuraj Linkeš the results of the setup and teardown steps in any way. 702e1f61839SJuraj Linkeš 703e1f61839SJuraj Linkeš Args: 704e1f61839SJuraj Linkeš result: The result of the test case. 705e1f61839SJuraj Linkeš error: The error that occurred in case of a failure. 706e1f61839SJuraj Linkeš """ 707e1f61839SJuraj Linkeš self.result = result 708e1f61839SJuraj Linkeš self.error = error 709e1f61839SJuraj Linkeš 710caae1889SJuraj Linkeš def _get_child_errors(self) -> list[Exception]: 711e1f61839SJuraj Linkeš if self.error: 712e1f61839SJuraj Linkeš return [self.error] 713e1f61839SJuraj Linkeš return [] 714e1f61839SJuraj Linkeš 715*9f8a2572STomáš Ďurovec def to_dict(self) -> TestCaseResultDict: 716*9f8a2572STomáš Ďurovec """Convert the test case result into a dictionary. 717*9f8a2572STomáš Ďurovec 718*9f8a2572STomáš Ďurovec The dictionary contains a test case name and the result name. 719*9f8a2572STomáš Ďurovec """ 720*9f8a2572STomáš Ďurovec return {"test_case_name": self.test_case_name, "result": self.result.name} 721*9f8a2572STomáš Ďurovec 722*9f8a2572STomáš Ďurovec def add_result(self, results: dict[str, int]): 723*9f8a2572STomáš Ďurovec r"""Add the test case result to the results. 724e1f61839SJuraj Linkeš 725e1f61839SJuraj Linkeš The base method goes through the hierarchy recursively and this method is here to stop 726*9f8a2572STomáš Ďurovec the recursion, as the :class:`TestCaseResult` are the leaves of the hierarchy tree. 727e1f61839SJuraj Linkeš 728e1f61839SJuraj Linkeš Args: 729*9f8a2572STomáš Ďurovec results: The dictionary to which results will be collated. 730e1f61839SJuraj Linkeš """ 731*9f8a2572STomáš Ďurovec results[self.result.name] += 1 732e1f61839SJuraj Linkeš 733566201aeSJuraj Linkeš def _mark_results(self, result) -> None: 734566201aeSJuraj Linkeš r"""Mark the result as `result`.""" 735566201aeSJuraj Linkeš self.update(result) 736caae1889SJuraj Linkeš 737e1f61839SJuraj Linkeš def __bool__(self) -> bool: 738e1f61839SJuraj Linkeš """The test case passed only if setup, teardown and the test case itself passed.""" 739e1f61839SJuraj Linkeš return bool(self.setup_result) and bool(self.teardown_result) and bool(self.result) 740e1f61839SJuraj Linkeš 741e1f61839SJuraj Linkeš 742*9f8a2572STomáš Ďurovecclass TextSummary: 743*9f8a2572STomáš Ďurovec """Generates and saves textual summaries of DTS run results. 744e1f61839SJuraj Linkeš 745*9f8a2572STomáš Ďurovec The summary includes: 746*9f8a2572STomáš Ďurovec * Results of test cases, 747*9f8a2572STomáš Ďurovec * Compiler version of the DPDK build, 748*9f8a2572STomáš Ďurovec * DPDK version of the DPDK source tree, 749*9f8a2572STomáš Ďurovec * Overall summary of results when multiple test runs are present. 750e1f61839SJuraj Linkeš """ 751e1f61839SJuraj Linkeš 752*9f8a2572STomáš Ďurovec _dict_result: DtsRunResultDict 753*9f8a2572STomáš Ďurovec _summary: dict[str, int | float] 754*9f8a2572STomáš Ďurovec _text: str 755*9f8a2572STomáš Ďurovec 756*9f8a2572STomáš Ďurovec def __init__(self, dts_run_result: DTSResult): 757*9f8a2572STomáš Ďurovec """Initializes with a DTSResult object and converts it to a dictionary format. 758e1f61839SJuraj Linkeš 759e1f61839SJuraj Linkeš Args: 760*9f8a2572STomáš Ďurovec dts_run_result: The DTS result. 761e1f61839SJuraj Linkeš """ 762*9f8a2572STomáš Ďurovec self._dict_result = dts_run_result.to_dict() 763*9f8a2572STomáš Ďurovec self._summary = self._dict_result["summary"] 764*9f8a2572STomáš Ďurovec self._text = "" 765e1f61839SJuraj Linkeš 766*9f8a2572STomáš Ďurovec @property 767*9f8a2572STomáš Ďurovec def _outdent(self) -> str: 768*9f8a2572STomáš Ďurovec """Appropriate indentation based on multiple test run results.""" 769*9f8a2572STomáš Ďurovec return "\t" if len(self._dict_result["test_runs"]) > 1 else "" 770566201aeSJuraj Linkeš 771*9f8a2572STomáš Ďurovec def save(self, output_path: Path): 772*9f8a2572STomáš Ďurovec """Generate and save text statistics to a file. 773e1f61839SJuraj Linkeš 774e1f61839SJuraj Linkeš Args: 775*9f8a2572STomáš Ďurovec output_path: The path where the text file will be saved. 776e1f61839SJuraj Linkeš """ 777*9f8a2572STomáš Ďurovec if self._dict_result["test_runs"]: 778*9f8a2572STomáš Ďurovec with open(f"{output_path}", "w") as fp: 779*9f8a2572STomáš Ďurovec self._add_test_runs_dict_decorator(self._add_test_run_dict) 780*9f8a2572STomáš Ďurovec fp.write(self._text) 781*9f8a2572STomáš Ďurovec 782*9f8a2572STomáš Ďurovec def _add_test_runs_dict_decorator(self, func: Callable): 783*9f8a2572STomáš Ďurovec """Handles multiple test runs and appends results to the summary. 784*9f8a2572STomáš Ďurovec 785*9f8a2572STomáš Ďurovec Adds headers for each test run and overall result when multiple 786*9f8a2572STomáš Ďurovec test runs are provided. 787*9f8a2572STomáš Ďurovec 788*9f8a2572STomáš Ďurovec Args: 789*9f8a2572STomáš Ďurovec func: Function to process and add results from each test run. 790*9f8a2572STomáš Ďurovec """ 791*9f8a2572STomáš Ďurovec if len(self._dict_result["test_runs"]) > 1: 792*9f8a2572STomáš Ďurovec for idx, test_run_result in enumerate(self._dict_result["test_runs"]): 793*9f8a2572STomáš Ďurovec self._text += f"TEST_RUN_{idx}\n" 794*9f8a2572STomáš Ďurovec func(test_run_result) 795*9f8a2572STomáš Ďurovec 796*9f8a2572STomáš Ďurovec self._add_overall_results() 797*9f8a2572STomáš Ďurovec else: 798*9f8a2572STomáš Ďurovec func(self._dict_result["test_runs"][0]) 799*9f8a2572STomáš Ďurovec 800*9f8a2572STomáš Ďurovec def _add_test_run_dict(self, test_run_dict: TestRunResultDict): 801*9f8a2572STomáš Ďurovec """Adds the results and the test run attributes of a single test run to the summary. 802*9f8a2572STomáš Ďurovec 803*9f8a2572STomáš Ďurovec Args: 804*9f8a2572STomáš Ďurovec test_run_dict: Dictionary containing the test run results. 805*9f8a2572STomáš Ďurovec """ 806*9f8a2572STomáš Ďurovec self._add_column( 807*9f8a2572STomáš Ďurovec DPDK_VERSION=test_run_dict["dpdk_version"], 808*9f8a2572STomáš Ďurovec COMPILER_VERSION=test_run_dict["compiler_version"], 809*9f8a2572STomáš Ďurovec **test_run_dict["summary"], 810e1f61839SJuraj Linkeš ) 811*9f8a2572STomáš Ďurovec self._text += "\n" 812e1f61839SJuraj Linkeš 813*9f8a2572STomáš Ďurovec def _add_column(self, **rows): 814*9f8a2572STomáš Ďurovec """Formats and adds key-value pairs to the summary text. 815*9f8a2572STomáš Ďurovec 816*9f8a2572STomáš Ďurovec Handles cases where values might be None by replacing them with "N/A". 817*9f8a2572STomáš Ďurovec 818*9f8a2572STomáš Ďurovec Args: 819*9f8a2572STomáš Ďurovec **rows: Arbitrary key-value pairs representing the result data. 820*9f8a2572STomáš Ďurovec """ 821*9f8a2572STomáš Ďurovec rows = {k: "N/A" if v is None else v for k, v in rows.items()} 822*9f8a2572STomáš Ďurovec max_length = len(max(rows, key=len)) 823*9f8a2572STomáš Ďurovec for key, value in rows.items(): 824*9f8a2572STomáš Ďurovec self._text += f"{self._outdent}{key:<{max_length}} = {value}\n" 825*9f8a2572STomáš Ďurovec 826*9f8a2572STomáš Ďurovec def _add_overall_results(self): 827*9f8a2572STomáš Ďurovec """Add overall summary of test runs.""" 828*9f8a2572STomáš Ďurovec self._text += "OVERALL\n" 829*9f8a2572STomáš Ďurovec self._add_column(**self._summary) 830*9f8a2572STomáš Ďurovec 831*9f8a2572STomáš Ďurovec 832*9f8a2572STomáš Ďurovecclass JsonResults: 833*9f8a2572STomáš Ďurovec """Save DTS run result in JSON format.""" 834*9f8a2572STomáš Ďurovec 835*9f8a2572STomáš Ďurovec _dict_result: DtsRunResultDict 836*9f8a2572STomáš Ďurovec 837*9f8a2572STomáš Ďurovec def __init__(self, dts_run_result: DTSResult): 838*9f8a2572STomáš Ďurovec """Initializes with a DTSResult object and converts it to a dictionary format. 839*9f8a2572STomáš Ďurovec 840*9f8a2572STomáš Ďurovec Args: 841*9f8a2572STomáš Ďurovec dts_run_result: The DTS result. 842*9f8a2572STomáš Ďurovec """ 843*9f8a2572STomáš Ďurovec self._dict_result = dts_run_result.to_dict() 844*9f8a2572STomáš Ďurovec 845*9f8a2572STomáš Ďurovec def save(self, output_path: Path): 846*9f8a2572STomáš Ďurovec """Save the result to a file as JSON. 847*9f8a2572STomáš Ďurovec 848*9f8a2572STomáš Ďurovec Args: 849*9f8a2572STomáš Ďurovec output_path: The path where the JSON file will be saved. 850*9f8a2572STomáš Ďurovec """ 851*9f8a2572STomáš Ďurovec with open(f"{output_path}", "w") as fp: 852*9f8a2572STomáš Ďurovec json.dump(self._dict_result, fp, indent=4) 853