<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 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_suite.py | 223 +++++++++++++++++++++++++++---------<br>
 1 file changed, 168 insertions(+), 55 deletions(-)<br>
<br>
diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py<br>
index d53553bf34..9e5251ffc6 100644<br>
--- a/dts/framework/test_suite.py<br>
+++ b/dts/framework/test_suite.py<br>
@@ -2,8 +2,19 @@<br>
 # Copyright(c) 2010-2014 Intel Corporation<br>
 # Copyright(c) 2023 PANTHEON.tech s.r.o.<br>
<br>
-"""<br>
-Base class for creating DTS test cases.<br>
+"""Features common to all test suites.<br>
+<br>
+The module defines the :class:`TestSuite` class which doesn't contain any test cases, and as such<br>
+must be extended by subclasses which add test cases. The :class:`TestSuite` contains the basics<br>
+needed by subclasses:<br>
+<br>
+    * Test suite and test case execution flow,<br>
+    * Testbed (SUT, TG) configuration,<br>
+    * Packet sending and verification,<br>
+    * Test case verification.<br>
+<br>
+The module also defines a function, :func:`get_test_suites`,<br>
+for gathering test suites from a Python module.<br>
 """<br>
<br>
 import importlib<br>
@@ -31,25 +42,44 @@<br>
<br>
<br>
 class TestSuite(object):<br>
-    """<br>
-    The base TestSuite class provides methods for handling basic flow of a test suite:<br>
-    * test case filtering and collection<br>
-    * test suite setup/cleanup<br>
-    * test setup/cleanup<br>
-    * test case execution<br>
-    * error handling and results storage<br>
-    Test cases are implemented by derived classes. Test cases are all methods<br>
-    starting with test_, further divided into performance test cases<br>
-    (starting with test_perf_) and functional test cases (all other test cases).<br>
-    By default, all test cases will be executed. A list of testcase str names<br>
-    may be specified in conf.yaml or on the command line<br>
-    to filter which test cases to run.<br>
-    The methods named [set_up|tear_down]_[suite|test_case] should be overridden<br>
-    in derived classes if the appropriate suite/test case fixtures are needed.<br>
+    """The base class with methods for handling the basic flow of a test suite.<br>
+<br>
+        * Test case filtering and collection,<br>
+        * Test suite setup/cleanup,<br>
+        * Test setup/cleanup,<br>
+        * Test case execution,<br>
+        * Error handling and results storage.<br>
+<br>
+    Test cases are implemented by subclasses. Test cases are all methods starting with ``test_``,<br>
+    further divided into performance test cases (starting with ``test_perf_``)<br>
+    and functional test cases (all other test cases).<br>
+<br>
+    By default, all test cases will be executed. A list of testcase names may be specified<br>
+    in the YAML test run configuration file and in the :option:`--test-cases` command line argument<br>
+    or in the :envvar:`DTS_TESTCASES` environment variable to filter which test cases to run.<br>
+    The union of both lists will be used. Any unknown test cases from the latter lists<br>
+    will be silently ignored.<br>
+<br>
+    If the :option:`--re-run` command line argument or the :envvar:`DTS_RERUN` environment variable<br>
+    is set, in case of a test case failure, the test case will be executed again until it passes<br>
+    or it fails that many times in addition of the first failure.<br>
+<br>
+    The methods named ``[set_up|tear_down]_[suite|test_case]`` should be overridden in subclasses<br>
+    if the appropriate test suite/test case fixtures are needed.<br>
+<br>
+    The test suite is aware of the testbed (the SUT and TG) it's running on. From this, it can<br>
+    properly choose the IP addresses and other configuration that must be tailored to the testbed.<br>
+<br>
+    Attributes:<br>
+        sut_node: The SUT node where the test suite is running.<br>
+        tg_node: The TG node where the test suite is running.<br>
+        is_blocking: Whether the test suite is blocking. A failure of a blocking test suite<br>
+            will block the execution of all subsequent test suites in the current build target.<br>
     """<br></blockquote><div><br></div><div><div style="font-family:arial,sans-serif" class="gmail_default">Should this attribute section instead be comments in the form "#:" because they are class variables instead of instance ones?<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>
     sut_node: SutNode<br>
-    is_blocking = False<br>
+    tg_node: TGNode<br>
+    is_blocking: bool = False<br>
     _logger: DTSLOG<br>
     _test_cases_to_run: list[str]<br>
     _func: bool<br>
@@ -72,6 +102,19 @@ def __init__(<br>
         func: bool,<br>
         build_target_result: BuildTargetResult,<br>
     ):<br>
+        """Initialize the test suite testbed information and basic configuration.<br>
+<br>
+        Process what test cases to run, create the associated :class:`TestSuiteResult`,<br>
+        find links between ports and set up default IP addresses to be used when configuring them.<br>
+<br>
+        Args:<br>
+            sut_node: The SUT node where the test suite will run.<br>
+            tg_node: The TG node where the test suite will run.<br>
+            test_cases: The list of test cases to execute.<br>
+                If empty, all test cases will be executed.<br>
+            func: Whether to run functional tests.<br>
+            build_target_result: The build target result this test suite is run in.<br>
+        """<br>
         self.sut_node = sut_node<br>
         self.tg_node = tg_node<br>
         self._logger = getLogger(self.__class__.__name__)<br>
@@ -95,6 +138,7 @@ def __init__(<br>
         self._tg_ip_address_ingress = ip_interface("<a href="http://192.168.101.3/24" rel="noreferrer" target="_blank">192.168.101.3/24</a>")<br>
<br>
     def _process_links(self) -> None:<br>
+        """Construct links between SUT and TG ports."""<br>
         for sut_port in self.sut_node.ports:<br>
             for tg_port in self.tg_node.ports:<br>
                 if (sut_port.identifier, sut_port.peer) == (<br>
@@ -106,27 +150,42 @@ def _process_links(self) -> None:<br>
                     )<br>
<br>
     def set_up_suite(self) -> None:<br>
-        """<br>
-        Set up test fixtures common to all test cases; this is done before<br>
-        any test case is run.<br>
+        """Set up test fixtures common to all test cases.<br>
+<br>
+        This is done before any test case has been run.<br>
         """<br>
<br>
     def tear_down_suite(self) -> None:<br>
-        """<br>
-        Tear down the previously created test fixtures common to all test cases.<br>
+        """Tear down the previously created test fixtures common to all test cases.<br>
+<br>
+        This is done after all test have been run.<br>
         """<br>
<br>
     def set_up_test_case(self) -> None:<br>
-        """<br>
-        Set up test fixtures before each test case.<br>
+        """Set up test fixtures before each test case.<br>
+<br>
+        This is done before *each* test case.<br>
         """<br>
<br>
     def tear_down_test_case(self) -> None:<br>
-        """<br>
-        Tear down the previously created test fixtures after each test case.<br>
+        """Tear down the previously created test fixtures after each test case.<br>
+<br>
+        This is done after *each* test case.<br>
         """<br>
<br>
     def configure_testbed_ipv4(self, restore: bool = False) -> None:<br>
+        """Configure IPv4 addresses on all testbed ports.<br>
+<br>
+        The configured ports are:<br>
+<br>
+        * SUT ingress port,<br>
+        * SUT egress port,<br>
+        * TG ingress port,<br>
+        * TG egress port.<br>
+<br>
+        Args:<br>
+            restore: If :data:`True`, will remove the configuration instead.<br>
+        """<br>
         delete = True if restore else False<br>
         enable = False if restore else True<br>
         self._configure_ipv4_forwarding(enable)<br>
@@ -153,11 +212,13 @@ def _configure_ipv4_forwarding(self, enable: bool) -> None:<br>
     def send_packet_and_capture(<br>
         self, packet: Packet, duration: float = 1<br>
     ) -> list[Packet]:<br>
-        """<br>
-        Send a packet through the appropriate interface and<br>
-        receive on the appropriate interface.<br>
-        Modify the packet with l3/l2 addresses corresponding<br>
-        to the testbed and desired traffic.<br>
+        """Send and receive `packet` using the associated TG.<br>
+<br>
+        Send `packet` through the appropriate interface and receive on the appropriate interface.<br>
+        Modify the packet with l3/l2 addresses corresponding to the testbed and desired traffic.<br>
+<br>
+        Returns:<br>
+            A list of received packets.<br>
         """<br>
         packet = self._adjust_addresses(packet)<br>
         return self.tg_node.send_packet_and_capture(<br>
@@ -165,13 +226,25 @@ def send_packet_and_capture(<br>
         )<br>
<br>
     def get_expected_packet(self, packet: Packet) -> Packet:<br>
+        """Inject the proper L2/L3 addresses into `packet`.<br>
+<br>
+        Args:<br>
+            packet: The packet to modify.<br>
+<br>
+        Returns:<br>
+            `packet` with injected L2/L3 addresses.<br>
+        """<br>
         return self._adjust_addresses(packet, expected=True)<br>
<br>
     def _adjust_addresses(self, packet: Packet, expected: bool = False) -> Packet:<br>
-        """<br>
+        """L2 and L3 address additions in both directions.<br>
+<br>
         Assumptions:<br>
-            Two links between SUT and TG, one link is TG -> SUT,<br>
-            the other SUT -> TG.<br>
+            Two links between SUT and TG, one link is TG -> SUT, the other SUT -> TG.<br>
+<br>
+        Args:<br>
+            packet: The packet to modify.<br>
+            expected: If :data:`True`, the direction is SUT -> TG, otherwise the direction is TG -> SUT.<br>
         """<br>
         if expected:<br>
             # The packet enters the TG from SUT<br>
@@ -197,6 +270,19 @@ def _adjust_addresses(self, packet: Packet, expected: bool = False) -> Packet:<br>
         return Ether(packet.build())<br>
<br>
     def verify(self, condition: bool, failure_description: str) -> None:<br>
+        """Verify `condition` and handle failures.<br>
+<br>
+        When `condition` is :data:`False`, raise an exception and log the last 10 commands<br>
+        executed on both the SUT and TG.<br>
+<br>
+        Args:<br>
+            condition: The condition to check.<br>
+            failure_description: A short description of the failure<br>
+                that will be stored in the raised exception.<br>
+<br>
+        Raises:<br>
+            TestCaseVerifyError: `condition` is :data:`False`.<br>
+        """<br>
         if not condition:<br>
             self._fail_test_case_verify(failure_description)<br>
<br>
@@ -216,6 +302,19 @@ def _fail_test_case_verify(self, failure_description: str) -> None:<br>
     def verify_packets(<br>
         self, expected_packet: Packet, received_packets: list[Packet]<br>
     ) -> None:<br>
+        """Verify that `expected_packet` has been received.<br>
+<br>
+        Go through `received_packets` and check that `expected_packet` is among them.<br>
+        If not, raise an exception and log the last 10 commands<br>
+        executed on both the SUT and TG.<br>
+<br>
+        Args:<br>
+            expected_packet: The packet we're expecting to receive.<br>
+            received_packets: The packets where we're looking for `expected_packet`.<br>
+<br>
+        Raises:<br>
+            TestCaseVerifyError: `expected_packet` is not among `received_packets`.<br>
+        """<br>
         for received_packet in received_packets:<br>
             if self._compare_packets(expected_packet, received_packet):<br>
                 break<br>
@@ -303,10 +402,14 @@ def _verify_l3_packet(self, received_packet: IP, expected_packet: IP) -> bool:<br>
         return True<br>
<br>
     def run(self) -> None:<br>
-        """<br>
-        Setup, execute and teardown the whole suite.<br>
-        Suite execution consists of running all test cases scheduled to be executed.<br>
-        A test cast run consists of setup, execution and teardown of said test case.<br>
+        """Set up, execute and tear down the whole suite.<br>
+<br>
+        Test suite execution consists of running all test cases scheduled to be executed.<br>
+        A test case run consists of setup, execution and teardown of said test case.<br>
+<br>
+        Record the setup and the teardown and handle failures.<br>
+<br>
+        The list of scheduled test cases is constructed when creating the :class:`TestSuite` object.<br>
         """<br>
         test_suite_name = self.__class__.__name__<br>
<br>
@@ -338,9 +441,7 @@ def run(self) -> None:<br>
                 raise BlockingTestSuiteError(test_suite_name)<br>
<br>
     def _execute_test_suite(self) -> None:<br>
-        """<br>
-        Execute all test cases scheduled to be executed in this suite.<br>
-        """<br>
+        """Execute all test cases scheduled to be executed in this suite."""<br>
         if self._func:<br>
             for test_case_method in self._get_functional_test_cases():<br>
                 test_case_name = test_case_method.__name__<br>
@@ -357,14 +458,18 @@ def _execute_test_suite(self) -> None:<br>
                     self._run_test_case(test_case_method, test_case_result)<br>
<br>
     def _get_functional_test_cases(self) -> list[MethodType]:<br>
-        """<br>
-        Get all functional test cases.<br>
+        """Get all functional test cases defined in this TestSuite.<br>
+<br>
+        Returns:<br>
+            The list of functional test cases of this TestSuite.<br>
         """<br>
         return self._get_test_cases(r"test_(?!perf_)")<br>
<br>
     def _get_test_cases(self, test_case_regex: str) -> list[MethodType]:<br>
-        """<br>
-        Return a list of test cases matching test_case_regex.<br>
+        """Return a list of test cases matching test_case_regex.<br>
+<br>
+        Returns:<br>
+            The list of test cases matching test_case_regex of this TestSuite.<br>
         """<br>
         self._logger.debug(f"Searching for test cases in {self.__class__.__name__}.")<br>
         filtered_test_cases = []<br>
@@ -378,9 +483,7 @@ def _get_test_cases(self, test_case_regex: str) -> list[MethodType]:<br>
         return filtered_test_cases<br>
<br>
     def _should_be_executed(self, test_case_name: str, test_case_regex: str) -> bool:<br>
-        """<br>
-        Check whether the test case should be executed.<br>
-        """<br>
+        """Check whether the test case should be scheduled to be executed."""<br>
         match = bool(re.match(test_case_regex, test_case_name))<br>
         if self._test_cases_to_run:<br>
             return match and test_case_name in self._test_cases_to_run<br>
@@ -390,9 +493,9 @@ def _should_be_executed(self, test_case_name: str, test_case_regex: str) -> bool<br>
     def _run_test_case(<br>
         self, test_case_method: MethodType, test_case_result: TestCaseResult<br>
     ) -> None:<br>
-        """<br>
-        Setup, execute and teardown a test case in this suite.<br>
-        Exceptions are caught and recorded in logs and results.<br>
+        """Setup, execute and teardown a test case in this suite.<br>
+<br>
+        Record the result of the setup and the teardown and handle failures.<br>
         """<br>
         test_case_name = test_case_method.__name__<br>
<br>
@@ -427,9 +530,7 @@ def _run_test_case(<br>
     def _execute_test_case(<br>
         self, test_case_method: MethodType, test_case_result: TestCaseResult<br>
     ) -> None:<br>
-        """<br>
-        Execute one test case and handle failures.<br>
-        """<br>
+        """Execute one test case, record the result and handle failures."""<br>
         test_case_name = test_case_method.__name__<br>
         try:<br>
             self._<a href="http://logger.info" rel="noreferrer" target="_blank">logger.info</a>(f"Starting test case execution: {test_case_name}")<br>
@@ -452,6 +553,18 @@ def _execute_test_case(<br>
<br>
<br>
 def get_test_suites(testsuite_module_path: str) -> list[type[TestSuite]]:<br>
+    r"""Find all :class:`TestSuite`\s in a Python module.<br>
+<br>
+    Args:<br>
+        testsuite_module_path: The path to the Python module.<br>
+<br>
+    Returns:<br>
+        The list of :class:`TestSuite`\s found within the Python module.<br>
+<br>
+    Raises:<br>
+        ConfigurationError: The test suite module was not found.<br>
+    """<br>
+<br>
     def is_test_suite(object: Any) -> bool:<br>
         try:<br>
             if issubclass(object, TestSuite) and object is not TestSuite:<br>
-- <br>
2.34.1<br>
<br>
</blockquote></div></div>