<div dir="ltr"><div><div style="font-family:arial,sans-serif" class="gmail_default">The only comments I had on this were a few places where I think attribute sections should be class variables instead. I tried to mark all of the places I saw it and it could be a difference where because of the way they are subclassed they might do it differently but I'm unsure.<br></div></div><div><br></div><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Wed, Nov 15, 2023 at 8:12 AM Juraj Linkeš <juraj.linkes@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">Format according to the Google format and PEP257, with slight<br>
deviations.<br>
<br>
Signed-off-by: Juraj Linkeš <juraj.linkes@pantheon.tech><br>
---<br>
dts/framework/test_result.py | 292 ++++++++++++++++++++++++++++-------<br>
1 file changed, 234 insertions(+), 58 deletions(-)<br>
<br>
diff --git a/dts/framework/test_result.py b/dts/framework/test_result.py<br>
index 603e18872c..05e210f6e7 100644<br>
--- a/dts/framework/test_result.py<br>
+++ b/dts/framework/test_result.py<br>
@@ -2,8 +2,25 @@<br>
# Copyright(c) 2023 PANTHEON.tech s.r.o.<br>
# Copyright(c) 2023 University of New Hampshire<br>
<br>
-"""<br>
-Generic result container and reporters<br>
+r"""Record and process DTS results.<br>
+<br>
+The results are recorded in a hierarchical manner:<br>
+<br>
+ * :class:`DTSResult` contains<br>
+ * :class:`ExecutionResult` contains<br>
+ * :class:`BuildTargetResult` contains<br>
+ * :class:`TestSuiteResult` contains<br>
+ * :class:`TestCaseResult`<br>
+<br>
+Each result may contain multiple lower level results, e.g. there are multiple<br>
+:class:`TestSuiteResult`\s in a :class:`BuildTargetResult`.<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>
+<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 os.path<br>
@@ -26,26 +43,34 @@<br>
<br>
<br>
class Result(Enum):<br>
- """<br>
- An Enum defining the possible states that<br>
- a setup, a teardown or a test case may end up in.<br>
- """<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>
+ #:<br>
SKIP = auto()<br>
<br>
def __bool__(self) -> bool:<br>
+ """Only PASS is True."""<br>
return self is self.PASS<br>
<br>
<br>
class FixtureResult(object):<br>
- """<br>
- A record that stored the result of a setup or a teardown.<br>
- The default is FAIL because immediately after creating the object<br>
- the setup of the corresponding stage will be executed, which also guarantees<br>
- the execution of teardown.<br>
+ """A record that stores the result of a setup or a teardown.<br>
+<br>
+ FAIL is a sensible default since it prevents false positives<br>
+ (which could happen if the default was PASS).<br>
+<br>
+ Preventing false positives or other false results is preferable since a failure<br>
+ is mostly likely to be investigated (the other false results may not be investigated at all).<br>
+<br>
+ Attributes:<br>
+ result: The associated result.<br>
+ error: The error in case of a failure.<br>
"""<br></blockquote><div><br></div><div><div style="font-family:arial,sans-serif" class="gmail_default">I think the items in the attributes section should instead be "#:" because they are class variables.<br></div></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>
result: Result<br>
@@ -56,21 +81,32 @@ def __init__(<br>
result: Result = Result.FAIL,<br>
error: Exception | None = None,<br>
):<br>
+ """Initialize the constructor with the fixture result and store a possible error.<br>
+<br>
+ Args:<br>
+ result: The result to store.<br>
+ error: The error which happened when a failure occurred.<br>
+ """<br>
self.result = result<br>
self.error = error<br>
<br>
def __bool__(self) -> bool:<br>
+ """A wrapper around the stored :class:`Result`."""<br>
return bool(self.result)<br>
<br>
<br>
class Statistics(dict):<br>
- """<br>
- A helper class used to store the number of test cases by its result<br>
- along a few other basic information.<br>
- Using a dict provides a convenient way to format the data.<br>
+ """How many test cases ended in which result state along some other basic information.<br>
+<br>
+ Subclassing :class:`dict` provides a convenient way to format the data.<br>
"""<br>
<br>
def __init__(self, dpdk_version: str | None):<br>
+ """Extend the constructor with relevant keys.<br>
+<br>
+ Args:<br>
+ dpdk_version: The version of tested DPDK.<br>
+ """<br></blockquote><div><br></div><div><div style="font-family:arial,sans-serif" class="gmail_default">Should we maybe mark the "PASS RATE" and the "DPDK VERSION" as instance variables of the class?<br></div></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">
super(Statistics, self).__init__()<br>
for result in Result:<br>
self[<a href="http://result.name" rel="noreferrer" target="_blank">result.name</a>] = 0<br>
@@ -78,8 +114,17 @@ def __init__(self, dpdk_version: str | None):<br>
self["DPDK VERSION"] = dpdk_version<br>
<br>
def __iadd__(self, other: Result) -> "Statistics":<br>
- """<br>
- Add a Result to the final count.<br>
+ """Add a Result to the final count.<br>
+<br>
+ Example:<br>
+ stats: Statistics = Statistics() # empty Statistics<br>
+ stats += Result.PASS # add a Result to `stats`<br>
+<br>
+ Args:<br>
+ other: The Result to add to this statistics object.<br>
+<br>
+ Returns:<br>
+ The modified statistics object.<br>
"""<br>
self[<a href="http://other.name" rel="noreferrer" target="_blank">other.name</a>] += 1<br>
self["PASS RATE"] = (<br>
@@ -90,9 +135,7 @@ def __iadd__(self, other: Result) -> "Statistics":<br>
return self<br>
<br>
def __str__(self) -> str:<br>
- """<br>
- Provide a string representation of the data.<br>
- """<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>
@@ -102,10 +145,16 @@ def __str__(self) -> str:<br>
<br>
<br>
class BaseResult(object):<br>
- """<br>
- The Base class for all results. Stores the results of<br>
- the setup and teardown portions of the corresponding stage<br>
- and a list of results from each inner stage in _inner_results.<br>
+ """Common data and behavior of DTS results.<br>
+<br>
+ Stores the results of the setup and teardown portions of the corresponding stage.<br>
+ The hierarchical nature of DTS results is captured recursively in an internal list.<br>
+ A stage is each level in this particular hierarchy (pre-execution or the top-most level,<br>
+ execution, build target, test suite and test case.)<br>
+<br>
+ Attributes:<br>
+ setup_result: The result of the setup of the particular stage.<br>
+ teardown_result: The results of the teardown of the particular stage.<br>
"""<br></blockquote><div><br></div><div><div style="font-family:arial,sans-serif" class="gmail_default">I think this might be another case of the attributes should be marked as class variables instead of instance variables.<br></div></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>
setup_result: FixtureResult<br>
@@ -113,15 +162,28 @@ class BaseResult(object):<br>
_inner_results: MutableSequence["BaseResult"]<br>
<br>
def __init__(self):<br>
+ """Initialize the constructor."""<br>
self.setup_result = FixtureResult()<br>
self.teardown_result = FixtureResult()<br>
self._inner_results = []<br>
<br>
def update_setup(self, result: Result, error: Exception | None = None) -> None:<br>
+ """Store the setup result.<br>
+<br>
+ Args:<br>
+ result: The result of the setup.<br>
+ error: The error that occurred in case of a failure.<br>
+ """<br>
self.setup_result.result = result<br>
self.setup_result.error = error<br>
<br>
def update_teardown(self, result: Result, error: Exception | None = None) -> None:<br>
+ """Store the teardown result.<br>
+<br>
+ Args:<br>
+ result: The result of the teardown.<br>
+ error: The error that occurred in case of a failure.<br>
+ """<br>
self.teardown_result.result = result<br>
self.teardown_result.error = error<br>
<br>
@@ -141,27 +203,55 @@ def _get_inner_errors(self) -> list[Exception]:<br>
]<br>
<br>
def get_errors(self) -> list[Exception]:<br>
+ """Compile errors from the whole result hierarchy.<br>
+<br>
+ Returns:<br>
+ The errors from setup, teardown and all errors found in the whole result hierarchy.<br>
+ """<br>
return self._get_setup_teardown_errors() + self._get_inner_errors()<br>
<br>
def add_stats(self, statistics: Statistics) -> None:<br>
+ """Collate stats from the whole result hierarchy.<br>
+<br>
+ Args:<br>
+ statistics: The :class:`Statistics` object where the stats will be collated.<br>
+ """<br>
for inner_result in self._inner_results:<br>
inner_result.add_stats(statistics)<br>
<br>
<br>
class TestCaseResult(BaseResult, FixtureResult):<br>
- """<br>
- The test case specific result.<br>
- Stores the result of the actual test case.<br>
- Also stores the test case name.<br>
+ r"""The test case specific result.<br>
+<br>
+ Stores the result of the actual test case. This is done by adding an extra superclass<br>
+ in :class:`FixtureResult`. The setup and teardown results are :class:`FixtureResult`\s and<br>
+ the class is itself a record of the test case.<br>
+<br>
+ Attributes:<br>
+ test_case_name: The test case name.<br>
"""<br>
<br></blockquote><div><br></div><div><div style="font-family:arial,sans-serif" class="gmail_default">Another spot where I think this should have a class variable comment.<br></div></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">
test_case_name: str<br>
<br>
def __init__(self, test_case_name: str):<br>
+ """Extend the constructor with `test_case_name`.<br>
+<br>
+ Args:<br>
+ test_case_name: The test case's name.<br>
+ """<br>
super(TestCaseResult, self).__init__()<br>
self.test_case_name = test_case_name<br>
<br>
def update(self, result: Result, error: Exception | None = None) -> None:<br>
+ """Update the test case result.<br>
+<br>
+ This updates the result of the test case itself and doesn't affect<br>
+ the results of the setup and teardown steps in any way.<br>
+<br>
+ Args:<br>
+ result: The result of the test case.<br>
+ error: The error that occurred in case of a failure.<br>
+ """<br>
self.result = result<br>
self.error = error<br>
<br>
@@ -171,38 +261,66 @@ def _get_inner_errors(self) -> list[Exception]:<br>
return []<br>
<br>
def add_stats(self, statistics: Statistics) -> None:<br>
+ r"""Add the test case result to statistics.<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>
+<br>
+ Args:<br>
+ statistics: The :class:`Statistics` object where the stats will be added.<br>
+ """<br>
statistics += self.result<br>
<br>
def __bool__(self) -> bool:<br>
+ """The test case passed only if setup, teardown and the test case itself passed."""<br>
return (<br>
bool(self.setup_result) and bool(self.teardown_result) and bool(self.result)<br>
)<br>
<br>
<br>
class TestSuiteResult(BaseResult):<br>
- """<br>
- The test suite specific result.<br>
- The _inner_results list stores results of test cases in a given test suite.<br>
- Also stores the test suite name.<br>
+ """The test suite specific result.<br>
+<br>
+ The internal list stores the results of all test cases in a given test suite.<br>
+<br>
+ Attributes:<br>
+ suite_name: The test suite name.<br>
"""<br>
<br></blockquote><div><br></div><div><div style="font-family:arial,sans-serif" class="gmail_default">I think this should also be a class variable.<br></div><br></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">
suite_name: str<br>
<br>
def __init__(self, suite_name: str):<br>
+ """Extend the constructor with `suite_name`.<br>
+<br>
+ Args:<br>
+ suite_name: The test suite's name.<br>
+ """<br>
super(TestSuiteResult, self).__init__()<br>
self.suite_name = suite_name<br>
<br>
def add_test_case(self, test_case_name: str) -> TestCaseResult:<br>
+ """Add and return the inner result (test case).<br>
+<br>
+ Returns:<br>
+ The test case's result.<br>
+ """<br>
test_case_result = TestCaseResult(test_case_name)<br>
self._inner_results.append(test_case_result)<br>
return test_case_result<br>
<br>
<br>
class BuildTargetResult(BaseResult):<br>
- """<br>
- The build target specific result.<br>
- The _inner_results list stores results of test suites in a given build target.<br>
- Also stores build target specifics, such as compiler used to build DPDK.<br>
+ """The build target specific result.<br>
+<br>
+ The internal list stores the results of all test suites in a given build target.<br>
+<br>
+ Attributes:<br>
+ arch: The DPDK build target architecture.<br>
+ os: The DPDK build target operating system.<br>
+ cpu: The DPDK build target CPU.<br>
+ compiler: The DPDK build target compiler.<br>
+ compiler_version: The DPDK build target compiler version.<br>
+ dpdk_version: The built DPDK version.<br>
"""<br></blockquote><div><br></div><div><div style="font-family:arial,sans-serif" class="gmail_default">I think this should be broken into class variables as well.<br></div></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>
arch: Architecture<br>
@@ -213,6 +331,11 @@ class BuildTargetResult(BaseResult):<br>
dpdk_version: str | None<br>
<br>
def __init__(self, build_target: BuildTargetConfiguration):<br>
+ """Extend the constructor with the `build_target`'s build target config.<br>
+<br>
+ Args:<br>
+ build_target: The build target's test run configuration.<br>
+ """<br>
super(BuildTargetResult, self).__init__()<br>
self.arch = build_target.arch<br>
self.os = build_target.os<br>
@@ -222,20 +345,35 @@ def __init__(self, build_target: BuildTargetConfiguration):<br>
self.dpdk_version = None<br>
<br>
def add_build_target_info(self, versions: BuildTargetInfo) -> None:<br>
+ """Add information about the build target gathered at runtime.<br>
+<br>
+ Args:<br>
+ versions: The additional information.<br>
+ """<br>
self.compiler_version = versions.compiler_version<br>
self.dpdk_version = versions.dpdk_version<br>
<br>
def add_test_suite(self, test_suite_name: str) -> TestSuiteResult:<br>
+ """Add and return the inner result (test suite).<br>
+<br>
+ Returns:<br>
+ The test suite's result.<br>
+ """<br>
test_suite_result = TestSuiteResult(test_suite_name)<br>
self._inner_results.append(test_suite_result)<br>
return test_suite_result<br>
<br>
<br>
class ExecutionResult(BaseResult):<br>
- """<br>
- The execution specific result.<br>
- The _inner_results list stores results of build targets in a given execution.<br>
- Also stores the SUT node configuration.<br>
+ """The execution specific result.<br>
+<br>
+ The internal list stores the results of all build targets in a given execution.<br>
+<br>
+ Attributes:<br>
+ sut_node: The SUT node used in the execution.<br>
+ sut_os_name: The operating system of the SUT node.<br>
+ sut_os_version: The operating system version of the SUT node.<br>
+ sut_kernel_version: The operating system kernel version of the SUT node.<br>
"""<br>
<br></blockquote><div><br></div><div><div style="font-family:arial,sans-serif" class="gmail_default">I think these should be class variables as well.<br></div></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">
sut_node: NodeConfiguration<br>
@@ -244,36 +382,55 @@ class ExecutionResult(BaseResult):<br>
sut_kernel_version: str<br>
<br>
def __init__(self, sut_node: NodeConfiguration):<br>
+ """Extend the constructor with the `sut_node`'s config.<br>
+<br>
+ Args:<br>
+ sut_node: The SUT node's test run configuration used in the execution.<br>
+ """<br>
super(ExecutionResult, self).__init__()<br>
self.sut_node = sut_node<br>
<br>
def add_build_target(<br>
self, build_target: BuildTargetConfiguration<br>
) -> BuildTargetResult:<br>
+ """Add and return the inner result (build target).<br>
+<br>
+ Args:<br>
+ build_target: The build target's test run configuration.<br>
+<br>
+ Returns:<br>
+ The build target's result.<br>
+ """<br>
build_target_result = BuildTargetResult(build_target)<br>
self._inner_results.append(build_target_result)<br>
return build_target_result<br>
<br>
def add_sut_info(self, sut_info: NodeInfo) -> None:<br>
+ """Add SUT information gathered at runtime.<br>
+<br>
+ Args:<br>
+ sut_info: The additional SUT node information.<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>
<br>
<br>
class DTSResult(BaseResult):<br>
- """<br>
- Stores environment information and test results from a DTS run, which are:<br>
- * Execution level information, such as SUT and TG hardware.<br>
- * Build target level information, such as compiler, target OS and cpu.<br>
- * Test suite results.<br>
- * All errors that are caught and recorded during DTS execution.<br>
+ """Stores environment information and test results from a DTS run.<br>
<br>
- The information is stored in nested objects.<br>
+ * Execution level information, such as testbed and the test suite list,<br>
+ * Build target level information, such as compiler, target OS and cpu,<br>
+ * Test suite and test case results,<br>
+ * All errors that are caught and recorded during DTS execution.<br>
<br>
- The class is capable of computing the return code used to exit DTS with<br>
- from the stored error.<br>
+ The information is stored hierarchically. This is the first level of the hierarchy<br>
+ and as such is where the data form the whole hierarchy is collated or processed.<br>
<br>
- It also provides a brief statistical summary of passed/failed test cases.<br>
+ The internal list stores the results of all executions.<br>
+<br>
+ Attributes:<br>
+ dpdk_version: The DPDK version to record.<br>
"""<br>
<br></blockquote><div><br></div><div><div style="font-family:arial,sans-serif" class="gmail_default">I think this should be a class variable as well.<br></div></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">
dpdk_version: str | None<br>
@@ -284,6 +441,11 @@ class DTSResult(BaseResult):<br>
_stats_filename: str<br>
<br>
def __init__(self, logger: DTSLOG):<br>
+ """Extend the constructor with top-level specifics.<br>
+<br>
+ Args:<br>
+ logger: The logger instance the whole result will use.<br>
+ """<br>
super(DTSResult, self).__init__()<br>
self.dpdk_version = None<br>
self._logger = logger<br>
@@ -293,21 +455,33 @@ def __init__(self, logger: DTSLOG):<br>
self._stats_filename = os.path.join(SETTINGS.output_dir, "statistics.txt")<br>
<br>
def add_execution(self, sut_node: NodeConfiguration) -> ExecutionResult:<br>
+ """Add and return the inner result (execution).<br>
+<br>
+ Args:<br>
+ sut_node: The SUT node's test run configuration.<br>
+<br>
+ Returns:<br>
+ The execution's result.<br>
+ """<br>
execution_result = ExecutionResult(sut_node)<br>
self._inner_results.append(execution_result)<br>
return execution_result<br>
<br>
def add_error(self, error: Exception) -> None:<br>
+ """Record an error that occurred outside any execution.<br>
+<br>
+ Args:<br>
+ error: The exception to record.<br>
+ """<br>
self._errors.append(error)<br>
<br>
def process(self) -> None:<br>
- """<br>
- Process the data after a DTS run.<br>
- The data is added to nested objects during runtime and this parent object<br>
- is not updated at that time. This requires us to process the nested data<br>
- after it's all been gathered.<br>
+ """Process the data after a whole DTS run.<br>
+<br>
+ The data is added to inner objects during runtime and this object is not updated<br>
+ at that time. This requires us to process the inner data after it's all been gathered.<br>
<br>
- The processing gathers all errors and the result statistics of test cases.<br>
+ The processing gathers all errors and the statistics of test case results.<br>
"""<br>
self._errors += self.get_errors()<br>
if self._errors and self._logger:<br>
@@ -321,8 +495,10 @@ def process(self) -> None:<br>
stats_file.write(str(self._stats_result))<br>
<br>
def get_return_code(self) -> int:<br>
- """<br>
- Go through all stored Exceptions and return the highest error code found.<br>
+ """Go through all stored Exceptions and return the final DTS error code.<br>
+<br>
+ Returns:<br>
+ The highest error code found.<br>
"""<br>
for error in self._errors:<br>
error_return_code = ErrorSeverity.GENERIC_ERR<br>
-- <br>
2.34.1<br>
<br>
</blockquote></div></div>