xref: /dpdk/dts/framework/test_result.py (revision 9f8a257235ac6d7ed5b621428551c88b4408ac26)
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