From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124]) by inbox.dpdk.org (Postfix) with ESMTP id D48F043349; Thu, 16 Nov 2023 23:16:31 +0100 (CET) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id BF691402E3; Thu, 16 Nov 2023 23:16:31 +0100 (CET) Received: from mail-pj1-f54.google.com (mail-pj1-f54.google.com [209.85.216.54]) by mails.dpdk.org (Postfix) with ESMTP id 7DB3040150 for ; Thu, 16 Nov 2023 23:16:29 +0100 (CET) Received: by mail-pj1-f54.google.com with SMTP id 98e67ed59e1d1-282ff1a97dcso1047385a91.1 for ; Thu, 16 Nov 2023 14:16:29 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=iol.unh.edu; s=unh-iol; t=1700172988; x=1700777788; darn=dpdk.org; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:from:to:cc:subject:date:message-id:reply-to; bh=uaaRXF0P8WuQcDwAINS9OgsrKiu10/g0C9f1DPTRaWg=; b=Cs528M7JId7Qr9Q8B5jNN9IBhS2ueZAcx2GDvj6Eez9ZN3cBuLX1PMHj0Fg6duLFa1 crX1bPdkCFewvDEf8dggsggKf/sTVZdqeolGE07+/En78zkWSFqMcFpR090Vqjonffvp jZc6JkqDppN9fbxiCUvaDarPg/hxLVrfl6GW4= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1700172988; x=1700777788; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=uaaRXF0P8WuQcDwAINS9OgsrKiu10/g0C9f1DPTRaWg=; b=wMXiu14wrxnj3HrEPm4TRQCEg4+rzb9OhxMay4hAJflaQikoZtie3l4UcHS7y8FfYQ j2i/Y/v8tfb1zutzWVixDcoGqJ4TNTWeSGeHSJVBjqqacdE0HrmGvfQMKR25tJYyFZsh TXPA+kMFUQuUWen4fgRm2qJW0dMrm3jtd+nqT9qt2IG81RGtFaScKt1Ze8u6+I+EnWI2 leogz1OmVuLb51hSbYZehsesIPpBtnodKYnSeFoTTS+Hj3U+ucdBZ6fmlpKKEOFu/xbC leoN+qsJoTIbGzRm7CL9IH+0qFYbPy/l0BuK8WJfcZaH/C19x9TA841abDh7/tsXP4wU DYRA== X-Gm-Message-State: AOJu0YyTIFsZX/1BMLP+vzDB/UVcpkC4neGbczNbEUf80rTaceNcBZhd Uy6DzbYEwGwQfJXTuGpmLz7MMyb9jce5xqO9Mnjkew== X-Google-Smtp-Source: AGHT+IGwR5ZyrEMC7bSJW4VZIRLB6GXXJmIN0slBC3ZQqKX4yV547gybkiDE4TmJ4e0UFIhSikqUci456gr7dEudGcs= X-Received: by 2002:a17:90b:1e02:b0:280:24a:9141 with SMTP id pg2-20020a17090b1e0200b00280024a9141mr17133686pjb.28.1700172988632; Thu, 16 Nov 2023 14:16:28 -0800 (PST) MIME-Version: 1.0 References: <20231108125324.191005-23-juraj.linkes@pantheon.tech> <20231115130959.39420-1-juraj.linkes@pantheon.tech> <20231115130959.39420-9-juraj.linkes@pantheon.tech> In-Reply-To: <20231115130959.39420-9-juraj.linkes@pantheon.tech> From: Jeremy Spewock Date: Thu, 16 Nov 2023 17:16:17 -0500 Message-ID: Subject: Re: [PATCH v7 08/21] dts: test suite docstring update To: =?UTF-8?Q?Juraj_Linke=C5=A1?= Cc: thomas@monjalon.net, Honnappa.Nagarahalli@arm.com, probb@iol.unh.edu, paul.szczepanek@arm.com, yoan.picchi@foss.arm.com, dev@dpdk.org Content-Type: multipart/alternative; boundary="0000000000000b6180060a4c5f0f" X-BeenThere: dev@dpdk.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: DPDK patches and discussions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dev-bounces@dpdk.org --0000000000000b6180060a4c5f0f Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable On Wed, Nov 15, 2023 at 8:12=E2=80=AFAM Juraj Linke=C5=A1 wrote: > Format according to the Google format and PEP257, with slight > deviations. > > Signed-off-by: Juraj Linke=C5=A1 > --- > dts/framework/test_suite.py | 223 +++++++++++++++++++++++++++--------- > 1 file changed, 168 insertions(+), 55 deletions(-) > > diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py > index d53553bf34..9e5251ffc6 100644 > --- a/dts/framework/test_suite.py > +++ b/dts/framework/test_suite.py > @@ -2,8 +2,19 @@ > # Copyright(c) 2010-2014 Intel Corporation > # Copyright(c) 2023 PANTHEON.tech s.r.o. > > -""" > -Base class for creating DTS test cases. > +"""Features common to all test suites. > + > +The module defines the :class:`TestSuite` class which doesn't contain an= y > test cases, and as such > +must be extended by subclasses which add test cases. The > :class:`TestSuite` contains the basics > +needed by subclasses: > + > + * Test suite and test case execution flow, > + * Testbed (SUT, TG) configuration, > + * Packet sending and verification, > + * Test case verification. > + > +The module also defines a function, :func:`get_test_suites`, > +for gathering test suites from a Python module. > """ > > import importlib > @@ -31,25 +42,44 @@ > > > class TestSuite(object): > - """ > - The base TestSuite class provides methods for handling basic flow of > a test suite: > - * test case filtering and collection > - * test suite setup/cleanup > - * test setup/cleanup > - * test case execution > - * error handling and results storage > - Test cases are implemented by derived classes. Test cases are all > methods > - starting with test_, further divided into performance test cases > - (starting with test_perf_) and functional test cases (all other test > cases). > - By default, all test cases will be executed. A list of testcase str > names > - may be specified in conf.yaml or on the command line > - to filter which test cases to run. > - The methods named [set_up|tear_down]_[suite|test_case] should be > overridden > - in derived classes if the appropriate suite/test case fixtures are > needed. > + """The base class with methods for handling the basic flow of a test > suite. > + > + * Test case filtering and collection, > + * Test suite setup/cleanup, > + * Test setup/cleanup, > + * Test case execution, > + * Error handling and results storage. > + > + Test cases are implemented by subclasses. Test cases are all methods > starting with ``test_``, > + further divided into performance test cases (starting with > ``test_perf_``) > + and functional test cases (all other test cases). > + > + By default, all test cases will be executed. A list of testcase name= s > may be specified > + in the YAML test run configuration file and in the > :option:`--test-cases` command line argument > + or in the :envvar:`DTS_TESTCASES` environment variable to filter > which test cases to run. > + The union of both lists will be used. Any unknown test cases from th= e > latter lists > + will be silently ignored. > + > + If the :option:`--re-run` command line argument or the > :envvar:`DTS_RERUN` environment variable > + is set, in case of a test case failure, the test case will be > executed again until it passes > + or it fails that many times in addition of the first failure. > + > + The methods named ``[set_up|tear_down]_[suite|test_case]`` should be > overridden in subclasses > + if the appropriate test suite/test case fixtures are needed. > + > + The test suite is aware of the testbed (the SUT and TG) it's running > on. From this, it can > + properly choose the IP addresses and other configuration that must b= e > tailored to the testbed. > + > + Attributes: > + sut_node: The SUT node where the test suite is running. > + tg_node: The TG node where the test suite is running. > + is_blocking: Whether the test suite is blocking. A failure of a > blocking test suite > + will block the execution of all subsequent test suites in th= e > current build target. > """ > Should this attribute section instead be comments in the form "#:" because they are class variables instead of instance ones? > > sut_node: SutNode > - is_blocking =3D False > + tg_node: TGNode > + is_blocking: bool =3D False > _logger: DTSLOG > _test_cases_to_run: list[str] > _func: bool > @@ -72,6 +102,19 @@ def __init__( > func: bool, > build_target_result: BuildTargetResult, > ): > + """Initialize the test suite testbed information and basic > configuration. > + > + Process what test cases to run, create the associated > :class:`TestSuiteResult`, > + find links between ports and set up default IP addresses to be > used when configuring them. > + > + Args: > + sut_node: The SUT node where the test suite will run. > + tg_node: The TG node where the test suite will run. > + test_cases: The list of test cases to execute. > + If empty, all test cases will be executed. > + func: Whether to run functional tests. > + build_target_result: The build target result this test suite > is run in. > + """ > self.sut_node =3D sut_node > self.tg_node =3D tg_node > self._logger =3D getLogger(self.__class__.__name__) > @@ -95,6 +138,7 @@ def __init__( > self._tg_ip_address_ingress =3D ip_interface("192.168.101.3/24") > > def _process_links(self) -> None: > + """Construct links between SUT and TG ports.""" > for sut_port in self.sut_node.ports: > for tg_port in self.tg_node.ports: > if (sut_port.identifier, sut_port.peer) =3D=3D ( > @@ -106,27 +150,42 @@ def _process_links(self) -> None: > ) > > def set_up_suite(self) -> None: > - """ > - Set up test fixtures common to all test cases; this is done befo= re > - any test case is run. > + """Set up test fixtures common to all test cases. > + > + This is done before any test case has been run. > """ > > def tear_down_suite(self) -> None: > - """ > - Tear down the previously created test fixtures common to all tes= t > cases. > + """Tear down the previously created test fixtures common to all > test cases. > + > + This is done after all test have been run. > """ > > def set_up_test_case(self) -> None: > - """ > - Set up test fixtures before each test case. > + """Set up test fixtures before each test case. > + > + This is done before *each* test case. > """ > > def tear_down_test_case(self) -> None: > - """ > - Tear down the previously created test fixtures after each test > case. > + """Tear down the previously created test fixtures after each tes= t > case. > + > + This is done after *each* test case. > """ > > def configure_testbed_ipv4(self, restore: bool =3D False) -> None: > + """Configure IPv4 addresses on all testbed ports. > + > + The configured ports are: > + > + * SUT ingress port, > + * SUT egress port, > + * TG ingress port, > + * TG egress port. > + > + Args: > + restore: If :data:`True`, will remove the configuration > instead. > + """ > delete =3D True if restore else False > enable =3D False if restore else True > self._configure_ipv4_forwarding(enable) > @@ -153,11 +212,13 @@ def _configure_ipv4_forwarding(self, enable: bool) > -> None: > def send_packet_and_capture( > self, packet: Packet, duration: float =3D 1 > ) -> list[Packet]: > - """ > - Send a packet through the appropriate interface and > - receive on the appropriate interface. > - Modify the packet with l3/l2 addresses corresponding > - to the testbed and desired traffic. > + """Send and receive `packet` using the associated TG. > + > + Send `packet` through the appropriate interface and receive on > the appropriate interface. > + Modify the packet with l3/l2 addresses corresponding to the > testbed and desired traffic. > + > + Returns: > + A list of received packets. > """ > packet =3D self._adjust_addresses(packet) > return self.tg_node.send_packet_and_capture( > @@ -165,13 +226,25 @@ def send_packet_and_capture( > ) > > def get_expected_packet(self, packet: Packet) -> Packet: > + """Inject the proper L2/L3 addresses into `packet`. > + > + Args: > + packet: The packet to modify. > + > + Returns: > + `packet` with injected L2/L3 addresses. > + """ > return self._adjust_addresses(packet, expected=3DTrue) > > def _adjust_addresses(self, packet: Packet, expected: bool =3D False= ) > -> Packet: > - """ > + """L2 and L3 address additions in both directions. > + > Assumptions: > - Two links between SUT and TG, one link is TG -> SUT, > - the other SUT -> TG. > + Two links between SUT and TG, one link is TG -> SUT, the > other SUT -> TG. > + > + Args: > + packet: The packet to modify. > + expected: If :data:`True`, the direction is SUT -> TG, > otherwise the direction is TG -> SUT. > """ > if expected: > # The packet enters the TG from SUT > @@ -197,6 +270,19 @@ def _adjust_addresses(self, packet: Packet, expected= : > bool =3D False) -> Packet: > return Ether(packet.build()) > > def verify(self, condition: bool, failure_description: str) -> None: > + """Verify `condition` and handle failures. > + > + When `condition` is :data:`False`, raise an exception and log th= e > last 10 commands > + executed on both the SUT and TG. > + > + Args: > + condition: The condition to check. > + failure_description: A short description of the failure > + that will be stored in the raised exception. > + > + Raises: > + TestCaseVerifyError: `condition` is :data:`False`. > + """ > if not condition: > self._fail_test_case_verify(failure_description) > > @@ -216,6 +302,19 @@ def _fail_test_case_verify(self, failure_description= : > str) -> None: > def verify_packets( > self, expected_packet: Packet, received_packets: list[Packet] > ) -> None: > + """Verify that `expected_packet` has been received. > + > + Go through `received_packets` and check that `expected_packet` i= s > among them. > + If not, raise an exception and log the last 10 commands > + executed on both the SUT and TG. > + > + Args: > + expected_packet: The packet we're expecting to receive. > + received_packets: The packets where we're looking for > `expected_packet`. > + > + Raises: > + TestCaseVerifyError: `expected_packet` is not among > `received_packets`. > + """ > for received_packet in received_packets: > if self._compare_packets(expected_packet, received_packet): > break > @@ -303,10 +402,14 @@ def _verify_l3_packet(self, received_packet: IP, > expected_packet: IP) -> bool: > return True > > def run(self) -> None: > - """ > - Setup, execute and teardown the whole suite. > - Suite execution consists of running all test cases scheduled to > be executed. > - A test cast run consists of setup, execution and teardown of sai= d > test case. > + """Set up, execute and tear down the whole suite. > + > + Test suite execution consists of running all test cases schedule= d > to be executed. > + A test case run consists of setup, execution and teardown of sai= d > test case. > + > + Record the setup and the teardown and handle failures. > + > + The list of scheduled test cases is constructed when creating th= e > :class:`TestSuite` object. > """ > test_suite_name =3D self.__class__.__name__ > > @@ -338,9 +441,7 @@ def run(self) -> None: > raise BlockingTestSuiteError(test_suite_name) > > def _execute_test_suite(self) -> None: > - """ > - Execute all test cases scheduled to be executed in this suite. > - """ > + """Execute all test cases scheduled to be executed in this > suite.""" > if self._func: > for test_case_method in self._get_functional_test_cases(): > test_case_name =3D test_case_method.__name__ > @@ -357,14 +458,18 @@ def _execute_test_suite(self) -> None: > self._run_test_case(test_case_method, > test_case_result) > > def _get_functional_test_cases(self) -> list[MethodType]: > - """ > - Get all functional test cases. > + """Get all functional test cases defined in this TestSuite. > + > + Returns: > + The list of functional test cases of this TestSuite. > """ > return self._get_test_cases(r"test_(?!perf_)") > > def _get_test_cases(self, test_case_regex: str) -> list[MethodType]: > - """ > - Return a list of test cases matching test_case_regex. > + """Return a list of test cases matching test_case_regex. > + > + Returns: > + The list of test cases matching test_case_regex of this > TestSuite. > """ > self._logger.debug(f"Searching for test cases in > {self.__class__.__name__}.") > filtered_test_cases =3D [] > @@ -378,9 +483,7 @@ def _get_test_cases(self, test_case_regex: str) -> > list[MethodType]: > return filtered_test_cases > > def _should_be_executed(self, test_case_name: str, test_case_regex: > str) -> bool: > - """ > - Check whether the test case should be executed. > - """ > + """Check whether the test case should be scheduled to be > executed.""" > match =3D bool(re.match(test_case_regex, test_case_name)) > if self._test_cases_to_run: > return match and test_case_name in self._test_cases_to_run > @@ -390,9 +493,9 @@ def _should_be_executed(self, test_case_name: str, > test_case_regex: str) -> bool > def _run_test_case( > self, test_case_method: MethodType, test_case_result: > TestCaseResult > ) -> None: > - """ > - Setup, execute and teardown a test case in this suite. > - Exceptions are caught and recorded in logs and results. > + """Setup, execute and teardown a test case in this suite. > + > + Record the result of the setup and the teardown and handle > failures. > """ > test_case_name =3D test_case_method.__name__ > > @@ -427,9 +530,7 @@ def _run_test_case( > def _execute_test_case( > self, test_case_method: MethodType, test_case_result: > TestCaseResult > ) -> None: > - """ > - Execute one test case and handle failures. > - """ > + """Execute one test case, record the result and handle > failures.""" > test_case_name =3D test_case_method.__name__ > try: > self._logger.info(f"Starting test case execution: > {test_case_name}") > @@ -452,6 +553,18 @@ def _execute_test_case( > > > def get_test_suites(testsuite_module_path: str) -> list[type[TestSuite]]= : > + r"""Find all :class:`TestSuite`\s in a Python module. > + > + Args: > + testsuite_module_path: The path to the Python module. > + > + Returns: > + The list of :class:`TestSuite`\s found within the Python module. > + > + Raises: > + ConfigurationError: The test suite module was not found. > + """ > + > def is_test_suite(object: Any) -> bool: > try: > if issubclass(object, TestSuite) and object is not TestSuite= : > -- > 2.34.1 > > --0000000000000b6180060a4c5f0f Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable


<= div dir=3D"ltr" class=3D"gmail_attr">On Wed, Nov 15, 2023 at 8:12=E2=80=AFA= M Juraj Linke=C5=A1 <juraj.linkes@pantheon.tech> wrote:
Format according to the Googl= e format and PEP257, with slight
deviations.

Signed-off-by: Juraj Linke=C5=A1 <juraj.linkes@pantheon.tech>
---
=C2=A0dts/framework/test_suite.py | 223 +++++++++++++++++++++++++++--------= -
=C2=A01 file changed, 168 insertions(+), 55 deletions(-)

diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py
index d53553bf34..9e5251ffc6 100644
--- a/dts/framework/test_suite.py
+++ b/dts/framework/test_suite.py
@@ -2,8 +2,19 @@
=C2=A0# Copyright(c) 2010-2014 Intel Corporation
=C2=A0# Copyright(c) 2023 PANTHEON.tech s.r.o.

-"""
-Base class for creating DTS test cases.
+"""Features common to all test suites.
+
+The module defines the :class:`TestSuite` class which doesn't contain = any test cases, and as such
+must be extended by subclasses which add test cases. The :class:`TestSuite= ` contains the basics
+needed by subclasses:
+
+=C2=A0 =C2=A0 * Test suite and test case execution flow,
+=C2=A0 =C2=A0 * Testbed (SUT, TG) configuration,
+=C2=A0 =C2=A0 * Packet sending and verification,
+=C2=A0 =C2=A0 * Test case verification.
+
+The module also defines a function, :func:`get_test_suites`,
+for gathering test suites from a Python module.
=C2=A0"""

=C2=A0import importlib
@@ -31,25 +42,44 @@


=C2=A0class TestSuite(object):
-=C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 The base TestSuite class provides methods for handling basic= flow of a test suite:
-=C2=A0 =C2=A0 * test case filtering and collection
-=C2=A0 =C2=A0 * test suite setup/cleanup
-=C2=A0 =C2=A0 * test setup/cleanup
-=C2=A0 =C2=A0 * test case execution
-=C2=A0 =C2=A0 * error handling and results storage
-=C2=A0 =C2=A0 Test cases are implemented by derived classes. Test cases ar= e all methods
-=C2=A0 =C2=A0 starting with test_, further divided into performance test c= ases
-=C2=A0 =C2=A0 (starting with test_perf_) and functional test cases (all ot= her test cases).
-=C2=A0 =C2=A0 By default, all test cases will be executed. A list of testc= ase str names
-=C2=A0 =C2=A0 may be specified in conf.yaml or on the command line
-=C2=A0 =C2=A0 to filter which test cases to run.
-=C2=A0 =C2=A0 The methods named [set_up|tear_down]_[suite|test_case] shoul= d be overridden
-=C2=A0 =C2=A0 in derived classes if the appropriate suite/test case fixtur= es are needed.
+=C2=A0 =C2=A0 """The base class with methods for handling t= he basic flow of a test suite.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 * Test case filtering and collection,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 * Test suite setup/cleanup,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 * Test setup/cleanup,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 * Test case execution,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 * Error handling and results storage.
+
+=C2=A0 =C2=A0 Test cases are implemented by subclasses. Test cases are all= methods starting with ``test_``,
+=C2=A0 =C2=A0 further divided into performance test cases (starting with `= `test_perf_``)
+=C2=A0 =C2=A0 and functional test cases (all other test cases).
+
+=C2=A0 =C2=A0 By default, all test cases will be executed. A list of testc= ase names may be specified
+=C2=A0 =C2=A0 in the YAML test run configuration file and in the :option:`= --test-cases` command line argument
+=C2=A0 =C2=A0 or in the :envvar:`DTS_TESTCASES` environment variable to fi= lter which test cases to run.
+=C2=A0 =C2=A0 The union of both lists will be used. Any unknown test cases= from the latter lists
+=C2=A0 =C2=A0 will be silently ignored.
+
+=C2=A0 =C2=A0 If the :option:`--re-run` command line argument or the :envv= ar:`DTS_RERUN` environment variable
+=C2=A0 =C2=A0 is set, in case of a test case failure, the test case will b= e executed again until it passes
+=C2=A0 =C2=A0 or it fails that many times in addition of the first failure= .
+
+=C2=A0 =C2=A0 The methods named ``[set_up|tear_down]_[suite|test_case]`` s= hould be overridden in subclasses
+=C2=A0 =C2=A0 if the appropriate test suite/test case fixtures are needed.=
+
+=C2=A0 =C2=A0 The test suite is aware of the testbed (the SUT and TG) it&#= 39;s running on. From this, it can
+=C2=A0 =C2=A0 properly choose the IP addresses and other configuration tha= t must be tailored to the testbed.
+
+=C2=A0 =C2=A0 Attributes:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 sut_node: The SUT node where the test suite is= running.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 tg_node: The TG node where the test suite is r= unning.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 is_blocking: Whether the test suite is blockin= g. A failure of a blocking test suite
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 will block the execution of all = subsequent test suites in the current build target.
=C2=A0 =C2=A0 =C2=A0"""

=
Should = this attribute section instead be comments in the form "#:" becau= se they are class variables instead of instance ones?
= =C2=A0

=C2=A0 =C2=A0 =C2=A0sut_node: SutNode
-=C2=A0 =C2=A0 is_blocking =3D False
+=C2=A0 =C2=A0 tg_node: TGNode
+=C2=A0 =C2=A0 is_blocking: bool =3D False
=C2=A0 =C2=A0 =C2=A0_logger: DTSLOG
=C2=A0 =C2=A0 =C2=A0_test_cases_to_run: list[str]
=C2=A0 =C2=A0 =C2=A0_func: bool
@@ -72,6 +102,19 @@ def __init__(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0func: bool,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0build_target_result: BuildTargetResult, =C2=A0 =C2=A0 =C2=A0):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Initialize the test suite te= stbed information and basic configuration.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Process what test cases to run, create the ass= ociated :class:`TestSuiteResult`,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 find links between ports and set up default IP= addresses to be used when configuring them.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 sut_node: The SUT node where the= test suite will run.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 tg_node: The TG node where the t= est suite will run.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 test_cases: The list of test cas= es to execute.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 If empty, all test= cases will be executed.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 func: Whether to run functional = tests.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 build_target_result: The build t= arget result this test suite is run in.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.sut_node =3D sut_node
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.tg_node =3D tg_node
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._logger =3D getLogger(self.__class__= .__name__)
@@ -95,6 +138,7 @@ def __init__(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._tg_ip_address_ingress =3D ip_interf= ace("192.168.101.3/24")

=C2=A0 =C2=A0 =C2=A0def _process_links(self) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Construct links between SUT = and TG ports."""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0for sut_port in self.sut_node.ports:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0for tg_port in self.tg_node= .ports:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if (sut_port.= identifier, sut_port.peer) =3D=3D (
@@ -106,27 +150,42 @@ def _process_links(self) -> None:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0)

=C2=A0 =C2=A0 =C2=A0def set_up_suite(self) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Set up test fixtures common to all test cases;= this is done before
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 any test case is run.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Set up test fixtures common = to all test cases.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 This is done before any test case has been run= .
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""

=C2=A0 =C2=A0 =C2=A0def tear_down_suite(self) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Tear down the previously created test fixtures= common to all test cases.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Tear down the previously cre= ated test fixtures common to all test cases.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 This is done after all test have been run.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""

=C2=A0 =C2=A0 =C2=A0def set_up_test_case(self) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Set up test fixtures before each test case. +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Set up test fixtures before = each test case.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 This is done before *each* test case.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""

=C2=A0 =C2=A0 =C2=A0def tear_down_test_case(self) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Tear down the previously created test fixtures= after each test case.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Tear down the previously cre= ated test fixtures after each test case.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 This is done after *each* test case.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""

=C2=A0 =C2=A0 =C2=A0def configure_testbed_ipv4(self, restore: bool =3D Fals= e) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Configure IPv4 addresses on = all testbed ports.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 The configured ports are:
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 * SUT ingress port,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 * SUT egress port,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 * TG ingress port,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 * TG egress port.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 restore: If :data:`True`, will r= emove the configuration instead.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0delete =3D True if restore else False
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0enable =3D False if restore else True
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._configure_ipv4_forwarding(enable) @@ -153,11 +212,13 @@ def _configure_ipv4_forwarding(self, enable: bool) -&= gt; None:
=C2=A0 =C2=A0 =C2=A0def send_packet_and_capture(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self, packet: Packet, duration: float =3D= 1
=C2=A0 =C2=A0 =C2=A0) -> list[Packet]:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Send a packet through the appropriate interfac= e and
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 receive on the appropriate interface.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Modify the packet with l3/l2 addresses corresp= onding
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 to the testbed and desired traffic.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Send and receive `packet` us= ing the associated TG.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Send `packet` through the appropriate interfac= e and receive on the appropriate interface.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Modify the packet with l3/l2 addresses corresp= onding to the testbed and desired traffic.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Returns:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 A list of received packets.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0packet =3D self._adjust_addresses(packet)=
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return self.tg_node.send_packet_and_captu= re(
@@ -165,13 +226,25 @@ def send_packet_and_capture(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)

=C2=A0 =C2=A0 =C2=A0def get_expected_packet(self, packet: Packet) -> Pac= ket:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Inject the proper L2/L3 addr= esses into `packet`.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 packet: The packet to modify. +
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Returns:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 `packet` with injected L2/L3 add= resses.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return self._adjust_addresses(packet, exp= ected=3DTrue)

=C2=A0 =C2=A0 =C2=A0def _adjust_addresses(self, packet: Packet, expected: b= ool =3D False) -> Packet:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """L2 and L3 address additions = in both directions.
+
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Assumptions:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Two links between SUT and TG, on= e link is TG -> SUT,
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 the other SUT -> TG.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Two links between SUT and TG, on= e link is TG -> SUT, the other SUT -> TG.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 packet: The packet to modify. +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 expected: If :data:`True`, the d= irection is SUT -> TG, otherwise the direction is TG -> SUT.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if expected:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0# The packet enters the TG = from SUT
@@ -197,6 +270,19 @@ def _adjust_addresses(self, packet: Packet, expected: = bool =3D False) -> Packet:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return Ether(packet.build())

=C2=A0 =C2=A0 =C2=A0def verify(self, condition: bool, failure_description: = str) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Verify `condition` and handl= e failures.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 When `condition` is :data:`False`, raise an ex= ception and log the last 10 commands
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 executed on both the SUT and TG.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 condition: The condition to chec= k.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 failure_description: A short des= cription of the failure
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 that will be store= d in the raised exception.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Raises:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 TestCaseVerifyError: `condition`= is :data:`False`.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if not condition:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._fail_test_case_verify= (failure_description)

@@ -216,6 +302,19 @@ def _fail_test_case_verify(self, failure_description: = str) -> None:
=C2=A0 =C2=A0 =C2=A0def verify_packets(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self, expected_packet: Packet, received_p= ackets: list[Packet]
=C2=A0 =C2=A0 =C2=A0) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Verify that `expected_packet= ` has been received.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Go through `received_packets` and check that `= expected_packet` is among them.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 If not, raise an exception and log the last 10= commands
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 executed on both the SUT and TG.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 expected_packet: The packet we&#= 39;re expecting to receive.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 received_packets: The packets wh= ere we're looking for `expected_packet`.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Raises:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 TestCaseVerifyError: `expected_p= acket` is not among `received_packets`.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0for received_packet in received_packets:<= br> =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if self._compare_packets(ex= pected_packet, received_packet):
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0break
@@ -303,10 +402,14 @@ def _verify_l3_packet(self, received_packet: IP, expe= cted_packet: IP) -> bool:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return True

=C2=A0 =C2=A0 =C2=A0def run(self) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Setup, execute and teardown the whole suite. -=C2=A0 =C2=A0 =C2=A0 =C2=A0 Suite execution consists of running all test c= ases scheduled to be executed.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 A test cast run consists of setup, execution a= nd teardown of said test case.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Set up, execute and tear dow= n the whole suite.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Test suite execution consists of running all t= est cases scheduled to be executed.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 A test case run consists of setup, execution a= nd teardown of said test case.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Record the setup and the teardown and handle f= ailures.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 The list of scheduled test cases is constructe= d when creating the :class:`TestSuite` object.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0test_suite_name =3D self.__class__.__name= __

@@ -338,9 +441,7 @@ def run(self) -> None:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0raise Blockin= gTestSuiteError(test_suite_name)

=C2=A0 =C2=A0 =C2=A0def _execute_test_suite(self) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Execute all test cases scheduled to be execute= d in this suite.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Execute all test cases sched= uled to be executed in this suite."""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if self._func:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0for test_case_method in sel= f._get_functional_test_cases():
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0test_case_nam= e =3D test_case_method.__name__
@@ -357,14 +458,18 @@ def _execute_test_suite(self) -> None:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0self._run_test_case(test_case_method, test_case_result)

=C2=A0 =C2=A0 =C2=A0def _get_functional_test_cases(self) -> list[MethodT= ype]:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Get all functional test cases.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Get all functional test case= s defined in this TestSuite.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Returns:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 The list of functional test case= s of this TestSuite.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return self._get_test_cases(r"test_(= ?!perf_)")

=C2=A0 =C2=A0 =C2=A0def _get_test_cases(self, test_case_regex: str) -> l= ist[MethodType]:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Return a list of test cases matching test_case= _regex.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Return a list of test cases = matching test_case_regex.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Returns:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 The list of test cases matching = test_case_regex of this TestSuite.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._logger.debug(f"Searching for t= est cases in {self.__class__.__name__}.")
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0filtered_test_cases =3D []
@@ -378,9 +483,7 @@ def _get_test_cases(self, test_case_regex: str) -> l= ist[MethodType]:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return filtered_test_cases

=C2=A0 =C2=A0 =C2=A0def _should_be_executed(self, test_case_name: str, test= _case_regex: str) -> bool:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Check whether the test case should be executed= .
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Check whether the test case = should be scheduled to be executed."""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0match =3D bool(re.match(test_case_regex, = test_case_name))
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if self._test_cases_to_run:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return match and test_case_= name in self._test_cases_to_run
@@ -390,9 +493,9 @@ def _should_be_executed(self, test_case_name: str, test= _case_regex: str) -> bool
=C2=A0 =C2=A0 =C2=A0def _run_test_case(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self, test_case_method: MethodType, test_= case_result: TestCaseResult
=C2=A0 =C2=A0 =C2=A0) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Setup, execute and teardown a test case in thi= s suite.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Exceptions are caught and recorded in logs and= results.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Setup, execute and teardown = a test case in this suite.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Record the result of the setup and the teardow= n and handle failures.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0test_case_name =3D test_case_method.__nam= e__

@@ -427,9 +530,7 @@ def _run_test_case(
=C2=A0 =C2=A0 =C2=A0def _execute_test_case(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self, test_case_method: MethodType, test_= case_result: TestCaseResult
=C2=A0 =C2=A0 =C2=A0) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Execute one test case and handle failures.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Execute one test case, recor= d the result and handle failures."""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0test_case_name =3D test_case_method.__nam= e__
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0try:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._logger.info(f"Start= ing test case execution: {test_case_name}")
@@ -452,6 +553,18 @@ def _execute_test_case(


=C2=A0def get_test_suites(testsuite_module_path: str) -> list[type[TestS= uite]]:
+=C2=A0 =C2=A0 r"""Find all :class:`TestSuite`\s in a Python= module.
+
+=C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 testsuite_module_path: The path to the Python = module.
+
+=C2=A0 =C2=A0 Returns:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 The list of :class:`TestSuite`\s found within = the Python module.
+
+=C2=A0 =C2=A0 Raises:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 ConfigurationError: The test suite module was = not found.
+=C2=A0 =C2=A0 """
+
=C2=A0 =C2=A0 =C2=A0def is_test_suite(object: Any) -> bool:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0try:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if issubclass(object, TestS= uite) and object is not TestSuite:
--
2.34.1

--0000000000000b6180060a4c5f0f--