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