<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>