On Fri, Jun 27, 2025 at 11:12 AM Thomas Wilks <thomas.wi...@arm.com> wrote:
> Refactor the DTS result recording system to use a hierarchical tree > structure based on `ResultNode` and `ResultLeaf`, replacing the prior flat > model of DTSResult, TestRunResult, and TestSuiteResult. This improves > clarity, composability, and enables consistent traversal and aggregation > of test outcomes. > Agreed. > > Update all FSM states and the runner to build results directly into the > tree, capturing setup, teardown, and test outcomes uniformly. Errors are > now stored directly as exceptions and reduced into an exit code, and > summaries are generated using Pydantic-based serializers for JSON and text > output. Finally, a new textual result summary is generated showing the > result of all the steps. > > Signed-off-by: Thomas Wilks <thomas.wi...@arm.com> > Signed-off-by: Luca Vizzarro <luca.vizza...@arm.com> > --- > dts/framework/runner.py | 33 +- > dts/framework/test_result.py | 882 +++++-------------- > dts/framework/test_run.py | 137 +-- > dts/framework/testbed_model/posix_session.py | 4 +- > 4 files changed, 337 insertions(+), 719 deletions(-) > > diff --git a/dts/framework/runner.py b/dts/framework/runner.py > index f20aa3576a..0a3d92b0c8 100644 > --- a/dts/framework/runner.py > +++ b/dts/framework/runner.py > @@ -18,16 +18,10 @@ > from framework.test_run import TestRun > from framework.testbed_model.node import Node > > -from .config import ( > - Configuration, > - load_config, > -) > +from .config import Configuration, load_config > from .logger import DTSLogger, get_dts_logger > from .settings import SETTINGS > -from .test_result import ( > - DTSResult, > - Result, > -) > +from .test_result import ResultNode, TestRunResult > > > class DTSRunner: > @@ -35,7 +29,7 @@ class DTSRunner: > > _configuration: Configuration > _logger: DTSLogger > - _result: DTSResult > + _result: TestRunResult > > def __init__(self): > """Initialize the instance with configuration, logger, result and > string constants.""" > @@ -54,7 +48,9 @@ def __init__(self): > if not os.path.exists(SETTINGS.output_dir): > os.makedirs(SETTINGS.output_dir) > self._logger.add_dts_root_logger_handlers(SETTINGS.verbose, > SETTINGS.output_dir) > - self._result = DTSResult(SETTINGS.output_dir, self._logger) > + > + test_suites_result = ResultNode(label="test_suites") > + self._result = TestRunResult(test_suites=test_suites_result) > > def run(self) -> None: > """Run DTS. > @@ -66,34 +62,30 @@ def run(self) -> None: > try: > # check the python version of the server that runs dts > self._check_dts_python_version() > - self._result.update_setup(Result.PASS) > > for node_config in self._configuration.nodes: > nodes.append(Node(node_config)) > > - test_run_result = > self._result.add_test_run(self._configuration.test_run) > test_run = TestRun( > self._configuration.test_run, > self._configuration.tests_config, > nodes, > - test_run_result, > + self._result, > ) > test_run.spin() > > except Exception as e: > - self._logger.exception("An unexpected error has occurred.") > + self._logger.exception("An unexpected error has occurred.", e) > self._result.add_error(e) > - # raise > > finally: > try: > self._logger.set_stage("post_run") > for node in nodes: > node.close() > - self._result.update_teardown(Result.PASS) > except Exception as e: > - self._logger.exception("The final cleanup of nodes > failed.") > - self._result.update_teardown(Result.ERROR, e) > + self._logger.exception("The final cleanup of nodes > failed.", e) > + self._result.add_error(e) > > # we need to put the sys.exit call outside the finally clause to > make sure > # that unexpected exceptions will propagate > @@ -116,9 +108,6 @@ def _check_dts_python_version(self) -> None: > > def _exit_dts(self) -> None: > """Process all errors and exit with the proper exit code.""" > - self._result.process() > - > if self._logger: > self._logger.info("DTS execution has ended.") > - > - sys.exit(self._result.get_return_code()) > + sys.exit(self._result.process()) > diff --git a/dts/framework/test_result.py b/dts/framework/test_result.py > index 7f576022c7..8ce6cc8fbf 100644 > --- a/dts/framework/test_result.py > +++ b/dts/framework/test_result.py > @@ -7,723 +7,323 @@ > > The results are recorded in a hierarchical manner: > > - * :class:`DTSResult` contains > * :class:`TestRunResult` contains > - * :class:`TestSuiteResult` contains > - * :class:`TestCaseResult` > + * :class:`ResultNode` may contain itself or > + * :class:`ResultLeaf` > > -Each result may contain multiple lower level results, e.g. there are > multiple > -:class:`TestSuiteResult`\s in a :class:`TestRunResult`. > -The results have common parts, such as setup and teardown results, > captured in :class:`BaseResult`, > -which also defines some common behaviors in its methods. > - > -Each result class has its own idiosyncrasies which they implement in > overridden methods. > +Each result may contain many intermediate steps, e.g. there are multiple > +:class:`ResultNode`\s in a :class:`ResultNode`. > > The :option:`--output` command line argument and the > :envvar:`DTS_OUTPUT_DIR` environment > variable modify the directory where the files with results will be stored. > """ > > -import json > -from collections.abc import MutableSequence > -from enum import Enum, auto > +import sys > +from collections import Counter > +from enum import IntEnum, auto > +from io import StringIO > from pathlib import Path > -from typing import Any, Callable, TypedDict > +from typing import Any, ClassVar, Literal, TextIO, Union > + > +from pydantic import ( > + BaseModel, > + ConfigDict, > + Field, > + computed_field, > + field_serializer, > + model_serializer, > +) > +from typing_extensions import OrderedDict > > -from .config.test_run import TestRunConfiguration > -from .exception import DTSError, ErrorSeverity > -from .logger import DTSLogger > -from .remote_session.dpdk import DPDKBuildInfo > -from .testbed_model.os_session import OSSessionInfo > -from .testbed_model.port import Port > +from framework.remote_session.dpdk import DPDKBuildInfo > +from framework.settings import SETTINGS > +from framework.testbed_model.os_session import OSSessionInfo > > +from .exception import DTSError, ErrorSeverity, InternalError > > -class Result(Enum): > + > +class Result(IntEnum): > """The possible states that a setup, a teardown or a test case may > end up in.""" > > #: > PASS = auto() > #: > - FAIL = auto() > - #: > - ERROR = auto() > + SKIP = auto() > #: > BLOCK = auto() > #: > - SKIP = auto() > + FAIL = auto() > + #: > + ERROR = auto() > > def __bool__(self) -> bool: > """Only :attr:`PASS` is True.""" > return self is self.PASS > > > -class TestCaseResultDict(TypedDict): > - """Represents the `TestCaseResult` results. > - > - Attributes: > - test_case_name: The name of the test case. > - result: The result name of the test case. > - """ > - > - test_case_name: str > - result: str > - > - > -class TestSuiteResultDict(TypedDict): > - """Represents the `TestSuiteResult` results. > - > - Attributes: > - test_suite_name: The name of the test suite. > - test_cases: A list of test case results contained in this test > suite. > - """ > - > - test_suite_name: str > - test_cases: list[TestCaseResultDict] > - > - > -class TestRunResultDict(TypedDict, total=False): > - """Represents the `TestRunResult` results. > - > - Attributes: > - compiler_version: The version of the compiler used for the DPDK > build. > - dpdk_version: The version of DPDK being tested. > - ports: A list of ports associated with the test run. > - test_suites: A list of test suite results included in this test > run. > - summary: A dictionary containing overall results, such as > pass/fail counts. > - """ > - > - compiler_version: str | None > - dpdk_version: str | None > - ports: list[dict[str, Any]] > - test_suites: list[TestSuiteResultDict] > - summary: dict[str, int | float] > - > +class ResultLeaf(BaseModel): > + """Class representing a result in the results tree. > > -class DtsRunResultDict(TypedDict): > - """Represents the `DtsRunResult` results. > + A leaf node that can contain the results for a > :class:`~.test_suite.TestSuite`, > + :class:`.test_suite.TestCase` or a DTS execution step. > > Attributes: > - test_runs: A list of test run results. > - summary: A summary dictionary containing overall statistics for > the test runs. > + result: The actual result. > + reason: The reason of the result. > """ > >From running some testcases that resulted in Error or Fail, I agree adding this is a big benefit. Providing an example for any others on the mailing list who are curious: rte_flow: FAIL test_drop_action_ETH: PASS test_drop_action_IP: PASS test_drop_action_L4: PASS test_drop_action_VLAN: PASS test_egress_rules: SKIP reason: flow rule failed validation. test_jump_action: PASS test_modify_actions: FAIL reason: Packet was never received. test_priority_attribute: PASS test_queue_action_ETH: PASS test_queue_action_IP: PASS test_queue_action_L4: PASS test_queue_action_VLAN: PASS > > > -- > 2.43.0 > > Looks good, I will apply to next-dts now. Reviewed-by: Patrick Robb <pr...@iol.unh.edu> Tested-by Patrick Robb <pr...@iol.unh.edu>