<div dir="ltr"><div dir="ltr"><div class="gmail_default" style="font-family:arial,sans-serif"><br></div></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Mon, Nov 20, 2023 at 11:33 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">On Thu, Nov 16, 2023 at 11:47 PM Jeremy Spewock <<a href="mailto:jspewock@iol.unh.edu" target="_blank">jspewock@iol.unh.edu</a>> wrote:<br>
><br>
> 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>
><br>
> On Wed, Nov 15, 2023 at 8:12 AM Juraj Linkeš <juraj.linkes@pantheon.tech> wrote:<br>
>><br>
>> 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>
><br>
><br>
> I think the items in the attributes section should instead be "#:" because they are class variables.<br>
><br>
<br>
Making these class variables would make the value the same for all<br>
instances, of which there are plenty. Why do you think these should be<br>
class variables?<br></blockquote><div><br></div><div><div class="gmail_default" style="font-family:arial,sans-serif">That explanation makes more sense. I guess I was thinking of class variables as anything we statically define as part of the class (i.e., like we say the class will always have a `result` and an `error` attribute), but I could have just been mistaken. Using the definition of instance variables as they can differ between instances I agree makes this comment and the other ones you touched on obsolete.</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>
>><br>
>><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>
><br>
><br>
> Should we maybe mark the "PASS RATE" and the "DPDK VERSION" as instance variables of the class?<br>
><br>
<br>
This is a dict, so these won't work as instance variables, but it<br>
makes sense to document these keys, so I'll add that.<br>
<br>
>><br>
>> 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>
><br>
><br>
> I think this might be another case of the attributes should be marked as class variables instead of instance variables.<br>
><br>
<br>
This is the same as in FixtureResult. For example, there could be<br>
multiple build targets with different results.<br>
<br>
>><br>
>><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>
><br>
> Another spot where I think this should have a class variable comment.<br>
><br>
>><br>
>> 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>
><br>
> I think this should also be a class variable.<br>
><br>
><br>
>><br>
>> 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>
><br>
><br>
> I think this should be broken into class variables as well.<br>
><br>
>><br>
>><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>
><br>
> I think these should be class variables as well.<br>
><br>
>><br>
>> 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>
><br>
> I think this should be a class variable as well.<br>
><br>
<br>
This is the only place where making this a class variable would work,<br>
but I don't see a reason for it. An instance variable works just as<br>
well.<br>
<br>
>><br>
>> 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>