<div dir="ltr">Adding Luca to this thread because it applies to him as well:<br><br>Hi Tomáš,<div><br></div><div>This all looks great, one thing I did notice when running locally is that the "test_suites" section of results.json contains null values instead of the names of the suites that were run. Could just be something strange happening on my side, but I would double check on your end just in case. Otherwise:</div><div><br></div><div>Reviewed-by: Dean Marx <<a href="mailto:dmarx@iol.unh.edu" target="_blank">dmarx@iol.unh.edu</a>></div></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Fri, Oct 25, 2024 at 1:59 PM Dean Marx <<a href="mailto:dmarx@iol.unh.edu">dmarx@iol.unh.edu</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"><div dir="ltr">Hi Tomáš,<div><br></div><div>This all looks great, one thing I did notice when running locally is that the "test_suites" section of results.json contains null values instead of the names of the suites that were run. Could just be something strange happening on my side, but I would double check on your end just in case. Otherwise:</div><div><br></div><div>Reviewed-by: Dean Marx <<a href="mailto:dmarx@iol.unh.edu" target="_blank">dmarx@iol.unh.edu</a>></div></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Mon, Sep 30, 2024 at 12:26 PM Tomáš Ďurovec <tomas.durovec@pantheon.tech> 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">The previous version of statistics store only the last test run<br>
attribute and result. In this patch we are adding header for each<br>
test run and overall summary of test runs at the end. This is<br>
represented as textual summaries with basic information and json<br>
result with more detailed information.<br>
<br>
Signed-off-by: Tomáš Ďurovec <tomas.durovec@pantheon.tech><br>
<br>
Depends-on: series-33184 ("DTS external DPDK build")<br>
---<br>
dts/framework/runner.py | 7 +-<br>
dts/framework/test_result.py | 409 ++++++++++++++++++++++++++++-------<br>
2 files changed, 332 insertions(+), 84 deletions(-)<br>
<br>
diff --git a/dts/framework/runner.py b/dts/framework/runner.py<br>
index 7d463c1fa1..be615ccace 100644<br>
--- a/dts/framework/runner.py<br>
+++ b/dts/framework/runner.py<br>
@@ -84,7 +84,7 @@ 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(self._logger)<br>
+ self._result = DTSResult(SETTINGS.output_dir, self._logger)<br>
self._test_suite_class_prefix = "Test"<br>
self._test_suite_module_prefix = "tests.TestSuite_"<br>
self._func_test_case_regex = r"test_(?!perf_)"<br>
@@ -421,11 +421,12 @@ def _run_test_run(<br>
self._<a href="http://logger.info" rel="noreferrer" target="_blank">logger.info</a>(<br>
f"Running test run with SUT '{<a href="http://test_run_config.system_under_test_node.name" rel="noreferrer" target="_blank">test_run_config.system_under_test_node.name</a>}'."<br>
)<br>
- test_run_result.add_sut_info(sut_node.node_info)<br>
+ test_run_result.ports = sut_node.ports<br>
+ test_run_result.sut_info = sut_node.node_info<br>
try:<br>
dpdk_location = SETTINGS.dpdk_location or test_run_config.dpdk_config.dpdk_location<br>
sut_node.set_up_test_run(test_run_config, dpdk_location)<br>
- test_run_result.add_dpdk_build_info(sut_node.get_dpdk_build_info())<br>
+ test_run_result.dpdk_build_info = sut_node.get_dpdk_build_info()<br>
tg_node.set_up_test_run(test_run_config, dpdk_location)<br>
test_run_result.update_setup(Result.PASS)<br>
except Exception as e:<br>
diff --git a/dts/framework/test_result.py b/dts/framework/test_result.py<br>
index 0a10723098..bf148a6b45 100644<br>
--- a/dts/framework/test_result.py<br>
+++ b/dts/framework/test_result.py<br>
@@ -22,18 +22,19 @@<br>
variable modify the directory where the files with results will be stored.<br>
"""<br>
<br>
-import os.path<br>
+import json<br>
from collections.abc import MutableSequence<br>
-from dataclasses import dataclass<br>
+from dataclasses import asdict, dataclass<br>
from enum import Enum, auto<br>
+from pathlib import Path<br>
from types import FunctionType<br>
-from typing import Union<br>
+from typing import Any, Callable, TypedDict<br>
<br>
from .config import DPDKBuildInfo, NodeInfo, TestRunConfiguration, TestSuiteConfig<br>
from .exception import DTSError, ErrorSeverity<br>
from .logger import DTSLogger<br>
-from .settings import SETTINGS<br>
from .test_suite import TestSuite<br>
+from .testbed_model.port import Port<br>
<br>
<br>
@dataclass(slots=True, frozen=True)<br>
@@ -85,6 +86,60 @@ def __bool__(self) -> bool:<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>
+<br>
+class DtsRunResultDict(TypedDict):<br>
+ """Represents the `DtsRunResult` results.<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>
+ """<br>
+<br>
+ test_runs: list[TestRunResultDict]<br>
+ summary: dict[str, int | float]<br>
+<br>
+<br>
class FixtureResult:<br>
"""A record that stores the result of a setup or a teardown.<br>
<br>
@@ -198,14 +253,34 @@ def get_errors(self) -> list[Exception]:<br>
"""<br>
return self._get_setup_teardown_errors() + self._get_child_errors()<br>
<br>
- def add_stats(self, statistics: "Statistics") -> None:<br>
- """Collate stats from the whole result hierarchy.<br>
+ def to_dict(self):<br>
+ """Convert the results hierarchy into a dictionary representation."""<br>
+<br>
+ def add_result(self, results: dict[str, int]):<br>
+ """Collate the test case result from the result hierarchy.<br>
<br>
Args:<br>
- statistics: The :class:`Statistics` object where the stats will be collated.<br>
+ results: The dictionary to which results will be collated.<br>
"""<br>
for child_result in self.child_results:<br>
- child_result.add_stats(statistics)<br>
+ child_result.add_result(results)<br>
+<br>
+ def generate_pass_rate_dict(self, test_run_summary) -> dict[str, float]:<br>
+ """Generate a dictionary with the FAIL/PASS ratio of all test cases.<br>
+<br>
+ Args:<br>
+ test_run_summary: The summary dictionary containing test result counts.<br>
+<br>
+ Returns:<br>
+ A dictionary with the FAIL/PASS ratio of all test cases.<br>
+ """<br>
+ return {<br>
+ "PASS_RATE": (<br>
+ float(test_run_summary[<a href="http://Result.PASS.name" rel="noreferrer" target="_blank">Result.PASS.name</a>])<br>
+ * 100<br>
+ / sum(test_run_summary[<a href="http://result.name" rel="noreferrer" target="_blank">result.name</a>] for result in Result)<br>
+ )<br>
+ }<br>
<br>
<br>
class DTSResult(BaseResult):<br>
@@ -220,31 +295,25 @@ class DTSResult(BaseResult):<br>
and as such is where the data form the whole hierarchy is collated or processed.<br>
<br>
The internal list stores the results of all test runs.<br>
-<br>
- Attributes:<br>
- dpdk_version: The DPDK version to record.<br>
"""<br>
<br>
- dpdk_version: str | None<br>
+ _output_dir: str<br>
_logger: DTSLogger<br>
_errors: list[Exception]<br>
_return_code: ErrorSeverity<br>
- _stats_result: Union["Statistics", None]<br>
- _stats_filename: str<br>
<br>
- def __init__(self, logger: DTSLogger):<br>
+ def __init__(self, output_dir: str, logger: DTSLogger):<br>
"""Extend the constructor with top-level specifics.<br>
<br>
Args:<br>
+ output_dir: The directory where DTS logs and results are saved.<br>
logger: The logger instance the whole result will use.<br>
"""<br>
super().__init__()<br>
- self.dpdk_version = None<br>
+ self._output_dir = output_dir<br>
self._logger = logger<br>
self._errors = []<br>
self._return_code = ErrorSeverity.NO_ERR<br>
- self._stats_result = None<br>
- self._stats_filename = os.path.join(SETTINGS.output_dir, "statistics.txt")<br>
<br>
def add_test_run(self, test_run_config: TestRunConfiguration) -> "TestRunResult":<br>
"""Add and return the child result (test run).<br>
@@ -281,10 +350,8 @@ def process(self) -> None:<br>
for error in self._errors:<br>
self._logger.debug(repr(error))<br>
<br>
- self._stats_result = Statistics(self.dpdk_version)<br>
- self.add_stats(self._stats_result)<br>
- with open(self._stats_filename, "w+") as stats_file:<br>
- stats_file.write(str(self._stats_result))<br>
+ TextSummary(self).save(Path(self._output_dir, "results_summary.txt"))<br>
+ JsonResults(self).save(Path(self._output_dir, "results.json"))<br>
<br>
def get_return_code(self) -> int:<br>
"""Go through all stored Exceptions and return the final DTS error code.<br>
@@ -302,6 +369,37 @@ def get_return_code(self) -> int:<br>
<br>
return int(self._return_code)<br>
<br>
+ def to_dict(self) -> DtsRunResultDict:<br>
+ """Convert DTS result into a dictionary format.<br>
+<br>
+ The dictionary contains test runs and summary of test runs.<br>
+<br>
+ Returns:<br>
+ A dictionary representation of the DTS result<br>
+ """<br>
+<br>
+ def merge_test_run_summaries(test_run_summaries: list[dict[str, int]]) -> dict[str, int]:<br>
+ """Merge multiple test run summaries into one dictionary.<br>
+<br>
+ Args:<br>
+ test_run_summaries: List of test run summary dictionaries.<br>
+<br>
+ Returns:<br>
+ A merged dictionary containing the aggregated summary.<br>
+ """<br>
+ return {<br>
+ <a href="http://key.name" rel="noreferrer" target="_blank">key.name</a>: sum(test_run_summary[<a href="http://key.name" rel="noreferrer" target="_blank">key.name</a>] for test_run_summary in test_run_summaries)<br>
+ for key in Result<br>
+ }<br>
+<br>
+ test_runs = [child.to_dict() for child in self.child_results]<br>
+ test_run_summary = merge_test_run_summaries([test_run["summary"] for test_run in test_runs])<br>
+<br>
+ return {<br>
+ "test_runs": test_runs,<br>
+ "summary": test_run_summary | self.generate_pass_rate_dict(test_run_summary),<br>
+ }<br>
+<br>
<br>
class TestRunResult(BaseResult):<br>
"""The test run specific result.<br>
@@ -316,13 +414,11 @@ class TestRunResult(BaseResult):<br>
sut_kernel_version: The operating system kernel version of the SUT node.<br>
"""<br>
<br>
- compiler_version: str | None<br>
- dpdk_version: str | None<br>
- sut_os_name: str<br>
- sut_os_version: str<br>
- sut_kernel_version: str<br>
_config: TestRunConfiguration<br>
_test_suites_with_cases: list[TestSuiteWithCases]<br>
+ _ports: list[Port]<br>
+ _sut_info: NodeInfo | None<br>
+ _dpdk_build_info: DPDKBuildInfo | None<br>
<br>
def __init__(self, test_run_config: TestRunConfiguration):<br>
"""Extend the constructor with the test run's config.<br>
@@ -331,10 +427,11 @@ def __init__(self, test_run_config: TestRunConfiguration):<br>
test_run_config: A test run configuration.<br>
"""<br>
super().__init__()<br>
- self.compiler_version = None<br>
- self.dpdk_version = None<br>
self._config = test_run_config<br>
self._test_suites_with_cases = []<br>
+ self._ports = []<br>
+ self._sut_info = None<br>
+ self._dpdk_build_info = None<br>
<br>
def add_test_suite(<br>
self,<br>
@@ -374,24 +471,96 @@ def test_suites_with_cases(self, test_suites_with_cases: list[TestSuiteWithCases<br>
)<br>
self._test_suites_with_cases = test_suites_with_cases<br>
<br>
- def add_sut_info(self, sut_info: NodeInfo) -> None:<br>
- """Add SUT information gathered at runtime.<br>
+ @property<br>
+ def ports(self) -> list[Port]:<br>
+ """Get the list of ports associated with this test run."""<br>
+ return self._ports<br>
+<br>
+ @ports.setter<br>
+ def ports(self, ports: list[Port]) -> None:<br>
+ """Set the list of ports associated with this test run.<br>
+<br>
+ Args:<br>
+ ports: The list of ports to associate with this test run.<br>
+<br>
+ Raises:<br>
+ ValueError: If the ports have already been assigned to this test run.<br>
+ """<br>
+ if self._ports:<br>
+ raise ValueError(<br>
+ "Attempted to assign `ports` to a test run result which already has `ports`."<br>
+ )<br>
+ self._ports = ports<br>
+<br>
+ @property<br>
+ def sut_info(self) -> NodeInfo | None:<br>
+ """Get the SUT node information associated with this test run."""<br>
+ return self._sut_info<br>
+<br>
+ @sut_info.setter<br>
+ def sut_info(self, sut_info: NodeInfo) -> None:<br>
+ """Set the SUT node information associated with this test run.<br>
<br>
Args:<br>
- sut_info: The additional SUT node information.<br>
+ sut_info: The SUT node information to associate with this test run.<br>
+<br>
+ Raises:<br>
+ ValueError: If the SUT information has already been assigned to this test run.<br>
"""<br>
- self.sut_os_name = sut_info.os_name<br>
- self.sut_os_version = sut_info.os_version<br>
- self.sut_kernel_version = sut_info.kernel_version<br>
+ if self._sut_info:<br>
+ raise ValueError(<br>
+ "Attempted to assign `sut_info` to a test run result which already has `sut_info`."<br>
+ )<br>
+ self._sut_info = sut_info<br>
<br>
- def add_dpdk_build_info(self, versions: DPDKBuildInfo) -> None:<br>
- """Add information about the DPDK build gathered at runtime.<br>
+ @property<br>
+ def dpdk_build_info(self) -> DPDKBuildInfo | None:<br>
+ """Get the DPDK build information associated with this test run."""<br>
+ return self._dpdk_build_info<br>
+<br>
+ @dpdk_build_info.setter<br>
+ def dpdk_build_info(self, dpdk_build_info: DPDKBuildInfo) -> None:<br>
+ """Set the DPDK build information associated with this test run.<br>
<br>
Args:<br>
- versions: The additional information.<br>
+ dpdk_build_info: The DPDK build information to associate with this test run.<br>
+<br>
+ Raises:<br>
+ ValueError: If the DPDK build information has already been assigned to this test run.<br>
"""<br>
- self.compiler_version = versions.compiler_version<br>
- self.dpdk_version = versions.dpdk_version<br>
+ if self._dpdk_build_info:<br>
+ raise ValueError(<br>
+ "Attempted to assign `dpdk_build_info` to a test run result which already "<br>
+ "has `dpdk_build_info`."<br>
+ )<br>
+ self._dpdk_build_info = dpdk_build_info<br>
+<br>
+ def to_dict(self) -> TestRunResultDict:<br>
+ """Convert the test run result into a dictionary.<br>
+<br>
+ The dictionary contains test suites in this test run, and a summary of the test run and<br>
+ information about the DPDK version, compiler version and associated ports.<br>
+<br>
+ Returns:<br>
+ TestRunResultDict: A dictionary representation of the test run result.<br>
+ """<br>
+ results = {<a href="http://result.name" rel="noreferrer" target="_blank">result.name</a>: 0 for result in Result}<br>
+ self.add_result(results)<br>
+<br>
+ compiler_version = None<br>
+ dpdk_version = None<br>
+<br>
+ if self.dpdk_build_info:<br>
+ compiler_version = self.dpdk_build_info.compiler_version<br>
+ dpdk_version = self.dpdk_build_info.dpdk_version<br>
+<br>
+ return {<br>
+ "compiler_version": compiler_version,<br>
+ "dpdk_version": dpdk_version,<br>
+ "ports": [asdict(port) for port in self.ports],<br>
+ "test_suites": [child.to_dict() for child in self.child_results],<br>
+ "summary": results | self.generate_pass_rate_dict(results),<br>
+ }<br>
<br>
def _block_result(self) -> None:<br>
r"""Mark the result as :attr:`~Result.BLOCK`\ed."""<br>
@@ -436,6 +605,16 @@ def add_test_case(self, test_case_name: str) -> "TestCaseResult":<br>
self.child_results.append(result)<br>
return result<br>
<br>
+ def to_dict(self) -> TestSuiteResultDict:<br>
+ """Convert the test suite result into a dictionary.<br>
+<br>
+ The dictionary contains a test suite name and test cases given in this test suite.<br>
+ """<br>
+ return {<br>
+ "test_suite_name": self.test_suite_name,<br>
+ "test_cases": [child.to_dict() for child in self.child_results],<br>
+ }<br>
+<br>
def _block_result(self) -> None:<br>
r"""Mark the result as :attr:`~Result.BLOCK`\ed."""<br>
for test_case_method in self._test_suite_with_cases.test_cases:<br>
@@ -483,16 +662,23 @@ def _get_child_errors(self) -> list[Exception]:<br>
return [self.error]<br>
return []<br>
<br>
- def add_stats(self, statistics: "Statistics") -> None:<br>
- r"""Add the test case result to statistics.<br>
+ def to_dict(self) -> TestCaseResultDict:<br>
+ """Convert the test case result into a dictionary.<br>
+<br>
+ The dictionary contains a test case name and the result name.<br>
+ """<br>
+ return {"test_case_name": self.test_case_name, "result": <a href="http://self.result.name" rel="noreferrer" target="_blank">self.result.name</a>}<br>
+<br>
+ def add_result(self, results: dict[str, int]):<br>
+ r"""Add the test case result to the results.<br>
<br>
The base method goes through the hierarchy recursively and this method is here to stop<br>
- the recursion, as the :class:`TestCaseResult`\s are the leaves of the hierarchy tree.<br>
+ the recursion, as the :class:`TestCaseResult` are the leaves of the hierarchy tree.<br>
<br>
Args:<br>
- statistics: The :class:`Statistics` object where the stats will be added.<br>
+ results: The dictionary to which results will be collated.<br>
"""<br>
- statistics += self.result<br>
+ results[<a href="http://self.result.name" rel="noreferrer" target="_blank">self.result.name</a>] += 1<br>
<br>
def _block_result(self) -> None:<br>
r"""Mark the result as :attr:`~Result.BLOCK`\ed."""<br>
@@ -503,53 +689,114 @@ def __bool__(self) -> bool:<br>
return bool(self.setup_result) and bool(self.teardown_result) and bool(self.result)<br>
<br>
<br>
-class Statistics(dict):<br>
- """How many test cases ended in which result state along some other basic information.<br>
+class TextSummary:<br>
+ """Generates and saves textual summaries of DTS run results.<br>
<br>
- Subclassing :class:`dict` provides a convenient way to format the data.<br>
+ The summary includes:<br>
+ * Results of test run test cases,<br>
+ * Compiler version of the DPDK build,<br>
+ * DPDK version of the DPDK source tree,<br>
+ * Overall summary of results when multiple test runs are present.<br>
+ """<br>
<br>
- The data are stored in the following keys:<br>
+ _dict_result: DtsRunResultDict<br>
+ _summary: dict[str, int | float]<br>
+ _text: str<br>
<br>
- * **PASS RATE** (:class:`int`) -- The FAIL/PASS ratio of all test cases.<br>
- * **DPDK VERSION** (:class:`str`) -- The tested DPDK version.<br>
- """<br>
+ def __init__(self, dts_run_result: DTSResult):<br>
+ """Initializes with a DTSResult object and converts it to a dictionary format.<br>
<br>
- def __init__(self, dpdk_version: str | None):<br>
- """Extend the constructor with keys in which the data are stored.<br>
+ Args:<br>
+ dts_run_result: The DTS result.<br>
+ """<br>
+ self._dict_result = dts_run_result.to_dict()<br>
+ self._summary = self._dict_result["summary"]<br>
+ self._text = ""<br>
+<br>
+ @property<br>
+ def _outdent(self) -> str:<br>
+ """Appropriate indentation based on multiple test run results."""<br>
+ return "\t" if len(self._dict_result["test_runs"]) > 1 else ""<br>
+<br>
+ def save(self, output_path: Path):<br>
+ """Generate and save text statistics to a file.<br>
<br>
Args:<br>
- dpdk_version: The version of tested DPDK.<br>
+ output_path: The path where the text file will be saved.<br>
"""<br>
- super().__init__()<br>
- for result in Result:<br>
- self[<a href="http://result.name" rel="noreferrer" target="_blank">result.name</a>] = 0<br>
- self["PASS RATE"] = 0.0<br>
- self["DPDK VERSION"] = dpdk_version<br>
+ if self._dict_result["test_runs"]:<br>
+ with open(f"{output_path}", "w") as fp:<br>
+ self._add_test_runs_dict_decorator(self._add_test_run_dict)<br>
+ fp.write(self._text)<br>
<br>
- def __iadd__(self, other: Result) -> "Statistics":<br>
- """Add a Result to the final count.<br>
+ def _add_test_runs_dict_decorator(self, func: Callable):<br>
+ """Handles multiple test runs and appends results to the summary.<br>
<br>
- Example:<br>
- stats: Statistics = Statistics() # empty Statistics<br>
- stats += Result.PASS # add a Result to `stats`<br>
+ Adds headers for each test run and overall result when multiple<br>
+ test runs are provided.<br>
<br>
Args:<br>
- other: The Result to add to this statistics object.<br>
+ func: Function to process and add results from each test run.<br>
+ """<br>
+ if len(self._dict_result["test_runs"]) > 1:<br>
+ for idx, test_run_result in enumerate(self._dict_result["test_runs"]):<br>
+ self._text += f"TEST_RUN_{idx}\n"<br>
+ func(test_run_result)<br>
<br>
- Returns:<br>
- The modified statistics object.<br>
+ self._add_overall_results()<br>
+ else:<br>
+ func(self._dict_result["test_runs"][0])<br>
+<br>
+ def _add_test_run_dict(self, test_run_dict: TestRunResultDict):<br>
+ """Adds the results and the test run attributes of a single test run to the summary.<br>
+<br>
+ Args:<br>
+ test_run_dict: Dictionary containing the test run results.<br>
"""<br>
- self[<a href="http://other.name" rel="noreferrer" target="_blank">other.name</a>] += 1<br>
- self["PASS RATE"] = (<br>
- float(self[<a href="http://Result.PASS.name" rel="noreferrer" target="_blank">Result.PASS.name</a>]) * 100 / sum(self[<a href="http://result.name" rel="noreferrer" target="_blank">result.name</a>] for result in Result)<br>
+ self._add_column(<br>
+ DPDK_VERSION=test_run_dict["dpdk_version"],<br>
+ COMPILER_VERSION=test_run_dict["compiler_version"],<br>
+ **test_run_dict["summary"],<br>
)<br>
- return self<br>
-<br>
- def __str__(self) -> str:<br>
- """Each line contains the formatted key = value pair."""<br>
- stats_str = ""<br>
- for key, value in self.items():<br>
- stats_str += f"{key:<12} = {value}\n"<br>
- # according to docs, we should use \n when writing to text files<br>
- # on all platforms<br>
- return stats_str<br>
+ self._text += "\n"<br>
+<br>
+ def _add_column(self, **rows):<br>
+ """Formats and adds key-value pairs to the summary text.<br>
+<br>
+ Handles cases where values might be None by replacing them with "N/A".<br>
+<br>
+ Args:<br>
+ **rows: Arbitrary key-value pairs representing the result data.<br>
+ """<br>
+ rows = {k: "N/A" if v is None else v for k, v in rows.items()}<br>
+ max_length = len(max(rows, key=len))<br>
+ for key, value in rows.items():<br>
+ self._text += f"{self._outdent}{key:<{max_length}} = {value}\n"<br>
+<br>
+ def _add_overall_results(self):<br>
+ """Add overall summary of test runs."""<br>
+ self._text += "OVERALL\n"<br>
+ self._add_column(**self._summary)<br>
+<br>
+<br>
+class JsonResults:<br>
+ """Save DTS run result in JSON format."""<br>
+<br>
+ _dict_result: DtsRunResultDict<br>
+<br>
+ def __init__(self, dts_run_result: DTSResult):<br>
+ """Initializes with a DTSResult object and converts it to a dictionary format.<br>
+<br>
+ Args:<br>
+ dts_run_result: The DTS result.<br>
+ """<br>
+ self._dict_result = dts_run_result.to_dict()<br>
+<br>
+ def save(self, output_path: Path):<br>
+ """Save the result to a file as JSON.<br>
+<br>
+ Args:<br>
+ output_path: The path where the JSON file will be saved.<br>
+ """<br>
+ with open(f"{output_path}", "w") as fp:<br>
+ json.dump(self._dict_result, fp, indent=4)<br>
-- <br>
2.46.1<br>
<br>
</blockquote></div>
</blockquote></div>