On Mon, Nov 20, 2023 at 11:33 AM Juraj Linkeš <juraj.linkes@pantheon.tech> wrote:
On Thu, Nov 16, 2023 at 11:47 PM Jeremy Spewock <jspewock@iol.unh.edu> wrote:
>
> 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.
>
> On Wed, Nov 15, 2023 at 8:12 AM Juraj Linkeš <juraj.linkes@pantheon.tech> wrote:
>>
>> Format according to the Google format and PEP257, with slight
>> deviations.
>>
>> Signed-off-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
>> ---
>>  dts/framework/test_result.py | 292 ++++++++++++++++++++++++++++-------
>>  1 file changed, 234 insertions(+), 58 deletions(-)
>>
>> diff --git a/dts/framework/test_result.py b/dts/framework/test_result.py
>> index 603e18872c..05e210f6e7 100644
>> --- a/dts/framework/test_result.py
>> +++ b/dts/framework/test_result.py
>> @@ -2,8 +2,25 @@
>>  # Copyright(c) 2023 PANTHEON.tech s.r.o.
>>  # Copyright(c) 2023 University of New Hampshire
>>
>> -"""
>> -Generic result container and reporters
>> +r"""Record and process DTS results.
>> +
>> +The results are recorded in a hierarchical manner:
>> +
>> +    * :class:`DTSResult` contains
>> +    * :class:`ExecutionResult` contains
>> +    * :class:`BuildTargetResult` contains
>> +    * :class:`TestSuiteResult` contains
>> +    * :class:`TestCaseResult`
>> +
>> +Each result may contain multiple lower level results, e.g. there are multiple
>> +:class:`TestSuiteResult`\s in a :class:`BuildTargetResult`.
>> +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.
>> +
>> +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 os.path
>> @@ -26,26 +43,34 @@
>>
>>
>>  class Result(Enum):
>> -    """
>> -    An Enum defining the possible states that
>> -    a setup, a teardown or a test case may end up in.
>> -    """
>> +    """The possible states that a setup, a teardown or a test case may end up in."""
>>
>> +    #:
>>      PASS = auto()
>> +    #:
>>      FAIL = auto()
>> +    #:
>>      ERROR = auto()
>> +    #:
>>      SKIP = auto()
>>
>>      def __bool__(self) -> bool:
>> +        """Only PASS is True."""
>>          return self is self.PASS
>>
>>
>>  class FixtureResult(object):
>> -    """
>> -    A record that stored the result of a setup or a teardown.
>> -    The default is FAIL because immediately after creating the object
>> -    the setup of the corresponding stage will be executed, which also guarantees
>> -    the execution of teardown.
>> +    """A record that stores the result of a setup or a teardown.
>> +
>> +    FAIL is a sensible default since it prevents false positives
>> +        (which could happen if the default was PASS).
>> +
>> +    Preventing false positives or other false results is preferable since a failure
>> +    is mostly likely to be investigated (the other false results may not be investigated at all).
>> +
>> +    Attributes:
>> +        result: The associated result.
>> +        error: The error in case of a failure.
>>      """
>
>
> I think the items in the attributes section should instead be "#:" because they are class variables.
>

Making these class variables would make the value the same for all
instances, of which there are plenty. Why do you think these should be
class variables?

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.
 

>>
>>
>>      result: Result
>> @@ -56,21 +81,32 @@ def __init__(
>>          result: Result = Result.FAIL,
>>          error: Exception | None = None,
>>      ):
>> +        """Initialize the constructor with the fixture result and store a possible error.
>> +
>> +        Args:
>> +            result: The result to store.
>> +            error: The error which happened when a failure occurred.
>> +        """
>>          self.result = result
>>          self.error = error
>>
>>      def __bool__(self) -> bool:
>> +        """A wrapper around the stored :class:`Result`."""
>>          return bool(self.result)
>>
>>
>>  class Statistics(dict):
>> -    """
>> -    A helper class used to store the number of test cases by its result
>> -    along a few other basic information.
>> -    Using a dict provides a convenient way to format the data.
>> +    """How many test cases ended in which result state along some other basic information.
>> +
>> +    Subclassing :class:`dict` provides a convenient way to format the data.
>>      """
>>
>>      def __init__(self, dpdk_version: str | None):
>> +        """Extend the constructor with relevant keys.
>> +
>> +        Args:
>> +            dpdk_version: The version of tested DPDK.
>> +        """
>
>
> Should we maybe mark the "PASS RATE" and the "DPDK VERSION" as instance variables of the class?
>

This is a dict, so these won't work as instance variables, but it
makes sense to document these keys, so I'll add that.

>>
>>          super(Statistics, self).__init__()
>>          for result in Result:
>>              self[result.name] = 0
>> @@ -78,8 +114,17 @@ def __init__(self, dpdk_version: str | None):
>>          self["DPDK VERSION"] = dpdk_version
>>
>>      def __iadd__(self, other: Result) -> "Statistics":
>> -        """
>> -        Add a Result to the final count.
>> +        """Add a Result to the final count.
>> +
>> +        Example:
>> +            stats: Statistics = Statistics()  # empty Statistics
>> +            stats += Result.PASS  # add a Result to `stats`
>> +
>> +        Args:
>> +            other: The Result to add to this statistics object.
>> +
>> +        Returns:
>> +            The modified statistics object.
>>          """
>>          self[other.name] += 1
>>          self["PASS RATE"] = (
>> @@ -90,9 +135,7 @@ def __iadd__(self, other: Result) -> "Statistics":
>>          return self
>>
>>      def __str__(self) -> str:
>> -        """
>> -        Provide a string representation of the data.
>> -        """
>> +        """Each line contains the formatted key = value pair."""
>>          stats_str = ""
>>          for key, value in self.items():
>>              stats_str += f"{key:<12} = {value}\n"
>> @@ -102,10 +145,16 @@ def __str__(self) -> str:
>>
>>
>>  class BaseResult(object):
>> -    """
>> -    The Base class for all results. Stores the results of
>> -    the setup and teardown portions of the corresponding stage
>> -    and a list of results from each inner stage in _inner_results.
>> +    """Common data and behavior of DTS results.
>> +
>> +    Stores the results of the setup and teardown portions of the corresponding stage.
>> +    The hierarchical nature of DTS results is captured recursively in an internal list.
>> +    A stage is each level in this particular hierarchy (pre-execution or the top-most level,
>> +    execution, build target, test suite and test case.)
>> +
>> +    Attributes:
>> +        setup_result: The result of the setup of the particular stage.
>> +        teardown_result: The results of the teardown of the particular stage.
>>      """
>
>
> I think this might be another case of the attributes should be marked as class variables instead of instance variables.
>

This is the same as in FixtureResult. For example, there could be
multiple build targets with different results.

>>
>>
>>      setup_result: FixtureResult
>> @@ -113,15 +162,28 @@ class BaseResult(object):
>>      _inner_results: MutableSequence["BaseResult"]
>>
>>      def __init__(self):
>> +        """Initialize the constructor."""
>>          self.setup_result = FixtureResult()
>>          self.teardown_result = FixtureResult()
>>          self._inner_results = []
>>
>>      def update_setup(self, result: Result, error: Exception | None = None) -> None:
>> +        """Store the setup result.
>> +
>> +        Args:
>> +            result: The result of the setup.
>> +            error: The error that occurred in case of a failure.
>> +        """
>>          self.setup_result.result = result
>>          self.setup_result.error = error
>>
>>      def update_teardown(self, result: Result, error: Exception | None = None) -> None:
>> +        """Store the teardown result.
>> +
>> +        Args:
>> +            result: The result of the teardown.
>> +            error: The error that occurred in case of a failure.
>> +        """
>>          self.teardown_result.result = result
>>          self.teardown_result.error = error
>>
>> @@ -141,27 +203,55 @@ def _get_inner_errors(self) -> list[Exception]:
>>          ]
>>
>>      def get_errors(self) -> list[Exception]:
>> +        """Compile errors from the whole result hierarchy.
>> +
>> +        Returns:
>> +            The errors from setup, teardown and all errors found in the whole result hierarchy.
>> +        """
>>          return self._get_setup_teardown_errors() + self._get_inner_errors()
>>
>>      def add_stats(self, statistics: Statistics) -> None:
>> +        """Collate stats from the whole result hierarchy.
>> +
>> +        Args:
>> +            statistics: The :class:`Statistics` object where the stats will be collated.
>> +        """
>>          for inner_result in self._inner_results:
>>              inner_result.add_stats(statistics)
>>
>>
>>  class TestCaseResult(BaseResult, FixtureResult):
>> -    """
>> -    The test case specific result.
>> -    Stores the result of the actual test case.
>> -    Also stores the test case name.
>> +    r"""The test case specific result.
>> +
>> +    Stores the result of the actual test case. This is done by adding an extra superclass
>> +    in :class:`FixtureResult`. The setup and teardown results are :class:`FixtureResult`\s and
>> +    the class is itself a record of the test case.
>> +
>> +    Attributes:
>> +        test_case_name: The test case name.
>>      """
>>
>
> Another spot where I think this should have a class variable comment.
>
>>
>>      test_case_name: str
>>
>>      def __init__(self, test_case_name: str):
>> +        """Extend the constructor with `test_case_name`.
>> +
>> +        Args:
>> +            test_case_name: The test case's name.
>> +        """
>>          super(TestCaseResult, self).__init__()
>>          self.test_case_name = test_case_name
>>
>>      def update(self, result: Result, error: Exception | None = None) -> None:
>> +        """Update the test case result.
>> +
>> +        This updates the result of the test case itself and doesn't affect
>> +        the results of the setup and teardown steps in any way.
>> +
>> +        Args:
>> +            result: The result of the test case.
>> +            error: The error that occurred in case of a failure.
>> +        """
>>          self.result = result
>>          self.error = error
>>
>> @@ -171,38 +261,66 @@ def _get_inner_errors(self) -> list[Exception]:
>>          return []
>>
>>      def add_stats(self, statistics: Statistics) -> None:
>> +        r"""Add the test case result to statistics.
>> +
>> +        The base method goes through the hierarchy recursively and this method is here to stop
>> +        the recursion, as the :class:`TestCaseResult`\s are the leaves of the hierarchy tree.
>> +
>> +        Args:
>> +            statistics: The :class:`Statistics` object where the stats will be added.
>> +        """
>>          statistics += self.result
>>
>>      def __bool__(self) -> bool:
>> +        """The test case passed only if setup, teardown and the test case itself passed."""
>>          return (
>>              bool(self.setup_result) and bool(self.teardown_result) and bool(self.result)
>>          )
>>
>>
>>  class TestSuiteResult(BaseResult):
>> -    """
>> -    The test suite specific result.
>> -    The _inner_results list stores results of test cases in a given test suite.
>> -    Also stores the test suite name.
>> +    """The test suite specific result.
>> +
>> +    The internal list stores the results of all test cases in a given test suite.
>> +
>> +    Attributes:
>> +        suite_name: The test suite name.
>>      """
>>
>
> I think this should also be a class variable.
>
>
>>
>>      suite_name: str
>>
>>      def __init__(self, suite_name: str):
>> +        """Extend the constructor with `suite_name`.
>> +
>> +        Args:
>> +            suite_name: The test suite's name.
>> +        """
>>          super(TestSuiteResult, self).__init__()
>>          self.suite_name = suite_name
>>
>>      def add_test_case(self, test_case_name: str) -> TestCaseResult:
>> +        """Add and return the inner result (test case).
>> +
>> +        Returns:
>> +            The test case's result.
>> +        """
>>          test_case_result = TestCaseResult(test_case_name)
>>          self._inner_results.append(test_case_result)
>>          return test_case_result
>>
>>
>>  class BuildTargetResult(BaseResult):
>> -    """
>> -    The build target specific result.
>> -    The _inner_results list stores results of test suites in a given build target.
>> -    Also stores build target specifics, such as compiler used to build DPDK.
>> +    """The build target specific result.
>> +
>> +    The internal list stores the results of all test suites in a given build target.
>> +
>> +    Attributes:
>> +        arch: The DPDK build target architecture.
>> +        os: The DPDK build target operating system.
>> +        cpu: The DPDK build target CPU.
>> +        compiler: The DPDK build target compiler.
>> +        compiler_version: The DPDK build target compiler version.
>> +        dpdk_version: The built DPDK version.
>>      """
>
>
> I think this should be broken into class variables as well.
>
>>
>>
>>      arch: Architecture
>> @@ -213,6 +331,11 @@ class BuildTargetResult(BaseResult):
>>      dpdk_version: str | None
>>
>>      def __init__(self, build_target: BuildTargetConfiguration):
>> +        """Extend the constructor with the `build_target`'s build target config.
>> +
>> +        Args:
>> +            build_target: The build target's test run configuration.
>> +        """
>>          super(BuildTargetResult, self).__init__()
>>          self.arch = build_target.arch
>>          self.os = build_target.os
>> @@ -222,20 +345,35 @@ def __init__(self, build_target: BuildTargetConfiguration):
>>          self.dpdk_version = None
>>
>>      def add_build_target_info(self, versions: BuildTargetInfo) -> None:
>> +        """Add information about the build target gathered at runtime.
>> +
>> +        Args:
>> +            versions: The additional information.
>> +        """
>>          self.compiler_version = versions.compiler_version
>>          self.dpdk_version = versions.dpdk_version
>>
>>      def add_test_suite(self, test_suite_name: str) -> TestSuiteResult:
>> +        """Add and return the inner result (test suite).
>> +
>> +        Returns:
>> +            The test suite's result.
>> +        """
>>          test_suite_result = TestSuiteResult(test_suite_name)
>>          self._inner_results.append(test_suite_result)
>>          return test_suite_result
>>
>>
>>  class ExecutionResult(BaseResult):
>> -    """
>> -    The execution specific result.
>> -    The _inner_results list stores results of build targets in a given execution.
>> -    Also stores the SUT node configuration.
>> +    """The execution specific result.
>> +
>> +    The internal list stores the results of all build targets in a given execution.
>> +
>> +    Attributes:
>> +        sut_node: The SUT node used in the execution.
>> +        sut_os_name: The operating system of the SUT node.
>> +        sut_os_version: The operating system version of the SUT node.
>> +        sut_kernel_version: The operating system kernel version of the SUT node.
>>      """
>>
>
> I think these should be class variables as well.
>
>>
>>      sut_node: NodeConfiguration
>> @@ -244,36 +382,55 @@ class ExecutionResult(BaseResult):
>>      sut_kernel_version: str
>>
>>      def __init__(self, sut_node: NodeConfiguration):
>> +        """Extend the constructor with the `sut_node`'s config.
>> +
>> +        Args:
>> +            sut_node: The SUT node's test run configuration used in the execution.
>> +        """
>>          super(ExecutionResult, self).__init__()
>>          self.sut_node = sut_node
>>
>>      def add_build_target(
>>          self, build_target: BuildTargetConfiguration
>>      ) -> BuildTargetResult:
>> +        """Add and return the inner result (build target).
>> +
>> +        Args:
>> +            build_target: The build target's test run configuration.
>> +
>> +        Returns:
>> +            The build target's result.
>> +        """
>>          build_target_result = BuildTargetResult(build_target)
>>          self._inner_results.append(build_target_result)
>>          return build_target_result
>>
>>      def add_sut_info(self, sut_info: NodeInfo) -> None:
>> +        """Add SUT information gathered at runtime.
>> +
>> +        Args:
>> +            sut_info: The additional SUT node information.
>> +        """
>>          self.sut_os_name = sut_info.os_name
>>          self.sut_os_version = sut_info.os_version
>>          self.sut_kernel_version = sut_info.kernel_version
>>
>>
>>  class DTSResult(BaseResult):
>> -    """
>> -    Stores environment information and test results from a DTS run, which are:
>> -    * Execution level information, such as SUT and TG hardware.
>> -    * Build target level information, such as compiler, target OS and cpu.
>> -    * Test suite results.
>> -    * All errors that are caught and recorded during DTS execution.
>> +    """Stores environment information and test results from a DTS run.
>>
>> -    The information is stored in nested objects.
>> +        * Execution level information, such as testbed and the test suite list,
>> +        * Build target level information, such as compiler, target OS and cpu,
>> +        * Test suite and test case results,
>> +        * All errors that are caught and recorded during DTS execution.
>>
>> -    The class is capable of computing the return code used to exit DTS with
>> -    from the stored error.
>> +    The information is stored hierarchically. This is the first level of the hierarchy
>> +    and as such is where the data form the whole hierarchy is collated or processed.
>>
>> -    It also provides a brief statistical summary of passed/failed test cases.
>> +    The internal list stores the results of all executions.
>> +
>> +    Attributes:
>> +        dpdk_version: The DPDK version to record.
>>      """
>>
>
> I think this should be a class variable as well.
>

This is the only place where making this a class variable would work,
but I don't see a reason for it. An instance variable works just as
well.

>>
>>      dpdk_version: str | None
>> @@ -284,6 +441,11 @@ class DTSResult(BaseResult):
>>      _stats_filename: str
>>
>>      def __init__(self, logger: DTSLOG):
>> +        """Extend the constructor with top-level specifics.
>> +
>> +        Args:
>> +            logger: The logger instance the whole result will use.
>> +        """
>>          super(DTSResult, self).__init__()
>>          self.dpdk_version = None
>>          self._logger = logger
>> @@ -293,21 +455,33 @@ def __init__(self, logger: DTSLOG):
>>          self._stats_filename = os.path.join(SETTINGS.output_dir, "statistics.txt")
>>
>>      def add_execution(self, sut_node: NodeConfiguration) -> ExecutionResult:
>> +        """Add and return the inner result (execution).
>> +
>> +        Args:
>> +            sut_node: The SUT node's test run configuration.
>> +
>> +        Returns:
>> +            The execution's result.
>> +        """
>>          execution_result = ExecutionResult(sut_node)
>>          self._inner_results.append(execution_result)
>>          return execution_result
>>
>>      def add_error(self, error: Exception) -> None:
>> +        """Record an error that occurred outside any execution.
>> +
>> +        Args:
>> +            error: The exception to record.
>> +        """
>>          self._errors.append(error)
>>
>>      def process(self) -> None:
>> -        """
>> -        Process the data after a DTS run.
>> -        The data is added to nested objects during runtime and this parent object
>> -        is not updated at that time. This requires us to process the nested data
>> -        after it's all been gathered.
>> +        """Process the data after a whole DTS run.
>> +
>> +        The data is added to inner objects during runtime and this object is not updated
>> +        at that time. This requires us to process the inner data after it's all been gathered.
>>
>> -        The processing gathers all errors and the result statistics of test cases.
>> +        The processing gathers all errors and the statistics of test case results.
>>          """
>>          self._errors += self.get_errors()
>>          if self._errors and self._logger:
>> @@ -321,8 +495,10 @@ def process(self) -> None:
>>              stats_file.write(str(self._stats_result))
>>
>>      def get_return_code(self) -> int:
>> -        """
>> -        Go through all stored Exceptions and return the highest error code found.
>> +        """Go through all stored Exceptions and return the final DTS error code.
>> +
>> +        Returns:
>> +            The highest error code found.
>>          """
>>          for error in self._errors:
>>              error_return_code = ErrorSeverity.GENERIC_ERR
>> --
>> 2.34.1
>>