xref: /dpdk/dts/framework/test_suite.py (revision e3ab9dd5cd5d5e7cb117507ba9580dae9706c1f5)
16fc05ca7SJuraj Linkeš# SPDX-License-Identifier: BSD-3-Clause
26fc05ca7SJuraj Linkeš# Copyright(c) 2010-2014 Intel Corporation
36fc05ca7SJuraj Linkeš# Copyright(c) 2023 PANTHEON.tech s.r.o.
4c64af3c7SLuca Vizzarro# Copyright(c) 2024 Arm Limited
56fc05ca7SJuraj Linkeš
66ef07151SJuraj Linkeš"""Features common to all test suites.
76ef07151SJuraj Linkeš
86ef07151SJuraj LinkešThe module defines the :class:`TestSuite` class which doesn't contain any test cases, and as such
96ef07151SJuraj Linkešmust be extended by subclasses which add test cases. The :class:`TestSuite` contains the basics
106ef07151SJuraj Linkešneeded by subclasses:
116ef07151SJuraj Linkeš
126ef07151SJuraj Linkeš    * Testbed (SUT, TG) configuration,
136ef07151SJuraj Linkeš    * Packet sending and verification,
146ef07151SJuraj Linkeš    * Test case verification.
156fc05ca7SJuraj Linkeš"""
166fc05ca7SJuraj Linkeš
17b6eb5004SJuraj Linkešimport inspect
180bf6796aSLuca Vizzarrofrom collections import Counter
19b6eb5004SJuraj Linkešfrom collections.abc import Callable, Sequence
20c64af3c7SLuca Vizzarrofrom dataclasses import dataclass
21b6eb5004SJuraj Linkešfrom enum import Enum, auto
22c64af3c7SLuca Vizzarrofrom functools import cached_property
23c64af3c7SLuca Vizzarrofrom importlib import import_module
24d99250aaSJuraj Linkešfrom ipaddress import IPv4Interface, IPv6Interface, ip_interface
25c64af3c7SLuca Vizzarrofrom pkgutil import iter_modules
26c64af3c7SLuca Vizzarrofrom types import ModuleType
27b6eb5004SJuraj Linkešfrom typing import ClassVar, Protocol, TypeVar, Union, cast
28d99250aaSJuraj Linkeš
29282688eaSLuca Vizzarrofrom scapy.layers.inet import IP  # type: ignore[import-untyped]
30282688eaSLuca Vizzarrofrom scapy.layers.l2 import Ether  # type: ignore[import-untyped]
310bf6796aSLuca Vizzarrofrom scapy.packet import Packet, Padding, raw  # type: ignore[import-untyped]
32c64af3c7SLuca Vizzarrofrom typing_extensions import Self
336fc05ca7SJuraj Linkeš
34566201aeSJuraj Linkešfrom framework.testbed_model.capability import TestProtocol
350b5ee16dSJuraj Linkešfrom framework.testbed_model.port import Port
362b2f5a8aSLuca Vizzarrofrom framework.testbed_model.sut_node import SutNode
372b2f5a8aSLuca Vizzarrofrom framework.testbed_model.tg_node import TGNode
38039256daSJuraj Linkešfrom framework.testbed_model.topology import Topology
392b2f5a8aSLuca Vizzarrofrom framework.testbed_model.traffic_generator.capturing_traffic_generator import (
402b2f5a8aSLuca Vizzarro    PacketFilteringConfig,
412b2f5a8aSLuca Vizzarro)
422b2f5a8aSLuca Vizzarro
43c64af3c7SLuca Vizzarrofrom .exception import ConfigurationError, InternalError, TestCaseVerifyError
4404f5a5a6SJuraj Linkešfrom .logger import DTSLogger, get_dts_logger
45c64af3c7SLuca Vizzarrofrom .utils import get_packet_summaries, to_pascal_case
466fc05ca7SJuraj Linkeš
476fc05ca7SJuraj Linkeš
48566201aeSJuraj Linkešclass TestSuite(TestProtocol):
495c7b6207SJuraj Linkeš    """The base class with building blocks needed by most test cases.
506ef07151SJuraj Linkeš
515c7b6207SJuraj Linkeš        * Test suite setup/cleanup methods to override,
525c7b6207SJuraj Linkeš        * Test case setup/cleanup methods to override,
535c7b6207SJuraj Linkeš        * Test case verification,
545c7b6207SJuraj Linkeš        * Testbed configuration,
555c7b6207SJuraj Linkeš        * Traffic sending and verification.
566ef07151SJuraj Linkeš
576ef07151SJuraj Linkeš    Test cases are implemented by subclasses. Test cases are all methods starting with ``test_``,
586ef07151SJuraj Linkeš    further divided into performance test cases (starting with ``test_perf_``)
596ef07151SJuraj Linkeš    and functional test cases (all other test cases).
606ef07151SJuraj Linkeš
616ef07151SJuraj Linkeš    By default, all test cases will be executed. A list of testcase names may be specified
624a4678c7SJuraj Linkeš    in the YAML test run configuration file and in the :option:`--test-suite` command line argument
636ef07151SJuraj Linkeš    or in the :envvar:`DTS_TESTCASES` environment variable to filter which test cases to run.
646ef07151SJuraj Linkeš    The union of both lists will be used. Any unknown test cases from the latter lists
656ef07151SJuraj Linkeš    will be silently ignored.
666ef07151SJuraj Linkeš
676ef07151SJuraj Linkeš    The methods named ``[set_up|tear_down]_[suite|test_case]`` should be overridden in subclasses
686ef07151SJuraj Linkeš    if the appropriate test suite/test case fixtures are needed.
696ef07151SJuraj Linkeš
706ef07151SJuraj Linkeš    The test suite is aware of the testbed (the SUT and TG) it's running on. From this, it can
716ef07151SJuraj Linkeš    properly choose the IP addresses and other configuration that must be tailored to the testbed.
726ef07151SJuraj Linkeš
736ef07151SJuraj Linkeš    Attributes:
746ef07151SJuraj Linkeš        sut_node: The SUT node where the test suite is running.
756ef07151SJuraj Linkeš        tg_node: The TG node where the test suite is running.
766fc05ca7SJuraj Linkeš    """
776fc05ca7SJuraj Linkeš
786fc05ca7SJuraj Linkeš    sut_node: SutNode
796ef07151SJuraj Linkeš    tg_node: TGNode
806ef07151SJuraj Linkeš    #: Whether the test suite is blocking. A failure of a blocking test suite
8111b2279aSTomáš Ďurovec    #: will block the execution of all subsequent test suites in the current test run.
826ef07151SJuraj Linkeš    is_blocking: ClassVar[bool] = False
8304f5a5a6SJuraj Linkeš    _logger: DTSLogger
84d99250aaSJuraj Linkeš    _sut_port_ingress: Port
85d99250aaSJuraj Linkeš    _sut_port_egress: Port
86d99250aaSJuraj Linkeš    _sut_ip_address_ingress: Union[IPv4Interface, IPv6Interface]
87d99250aaSJuraj Linkeš    _sut_ip_address_egress: Union[IPv4Interface, IPv6Interface]
88d99250aaSJuraj Linkeš    _tg_port_ingress: Port
89d99250aaSJuraj Linkeš    _tg_port_egress: Port
90d99250aaSJuraj Linkeš    _tg_ip_address_ingress: Union[IPv4Interface, IPv6Interface]
91d99250aaSJuraj Linkeš    _tg_ip_address_egress: Union[IPv4Interface, IPv6Interface]
926fc05ca7SJuraj Linkeš
936fc05ca7SJuraj Linkeš    def __init__(
946fc05ca7SJuraj Linkeš        self,
956fc05ca7SJuraj Linkeš        sut_node: SutNode,
96cecfe0aaSJuraj Linkeš        tg_node: TGNode,
970b5ee16dSJuraj Linkeš        topology: Topology,
986fc05ca7SJuraj Linkeš    ):
996ef07151SJuraj Linkeš        """Initialize the test suite testbed information and basic configuration.
1006ef07151SJuraj Linkeš
1015d094f9fSJuraj Linkeš        Find links between ports and set up default IP addresses to be used when
1025d094f9fSJuraj Linkeš        configuring them.
1036ef07151SJuraj Linkeš
1046ef07151SJuraj Linkeš        Args:
1056ef07151SJuraj Linkeš            sut_node: The SUT node where the test suite will run.
1066ef07151SJuraj Linkeš            tg_node: The TG node where the test suite will run.
1070b5ee16dSJuraj Linkeš            topology: The topology where the test suite will run.
1086ef07151SJuraj Linkeš        """
1096fc05ca7SJuraj Linkeš        self.sut_node = sut_node
110cecfe0aaSJuraj Linkeš        self.tg_node = tg_node
11104f5a5a6SJuraj Linkeš        self._logger = get_dts_logger(self.__class__.__name__)
1120b5ee16dSJuraj Linkeš        self._tg_port_egress = topology.tg_port_egress
1130b5ee16dSJuraj Linkeš        self._sut_port_ingress = topology.sut_port_ingress
1140b5ee16dSJuraj Linkeš        self._sut_port_egress = topology.sut_port_egress
1150b5ee16dSJuraj Linkeš        self._tg_port_ingress = topology.tg_port_ingress
116d99250aaSJuraj Linkeš        self._sut_ip_address_ingress = ip_interface("192.168.100.2/24")
117d99250aaSJuraj Linkeš        self._sut_ip_address_egress = ip_interface("192.168.101.2/24")
118d99250aaSJuraj Linkeš        self._tg_ip_address_egress = ip_interface("192.168.100.3/24")
119d99250aaSJuraj Linkeš        self._tg_ip_address_ingress = ip_interface("192.168.101.3/24")
120d99250aaSJuraj Linkeš
121b6eb5004SJuraj Linkeš    @classmethod
122c64af3c7SLuca Vizzarro    def get_test_cases(cls) -> list[type["TestCase"]]:
123c64af3c7SLuca Vizzarro        """A list of all the available test cases."""
124c64af3c7SLuca Vizzarro
125c64af3c7SLuca Vizzarro        def is_test_case(function: Callable) -> bool:
126c64af3c7SLuca Vizzarro            if inspect.isfunction(function):
127c64af3c7SLuca Vizzarro                # TestCase is not used at runtime, so we can't use isinstance() with `function`.
128c64af3c7SLuca Vizzarro                # But function.test_type exists.
129c64af3c7SLuca Vizzarro                if hasattr(function, "test_type"):
130c64af3c7SLuca Vizzarro                    return isinstance(function.test_type, TestCaseType)
131c64af3c7SLuca Vizzarro            return False
132c64af3c7SLuca Vizzarro
133c64af3c7SLuca Vizzarro        return [test_case for _, test_case in inspect.getmembers(cls, is_test_case)]
134c64af3c7SLuca Vizzarro
135c64af3c7SLuca Vizzarro    @classmethod
136c64af3c7SLuca Vizzarro    def filter_test_cases(
137b6eb5004SJuraj Linkeš        cls, test_case_sublist: Sequence[str] | None = None
138b6eb5004SJuraj Linkeš    ) -> tuple[set[type["TestCase"]], set[type["TestCase"]]]:
139c64af3c7SLuca Vizzarro        """Filter `test_case_sublist` from this class.
140b6eb5004SJuraj Linkeš
141b6eb5004SJuraj Linkeš        Test cases are regular (or bound) methods decorated with :func:`func_test`
142b6eb5004SJuraj Linkeš        or :func:`perf_test`.
143b6eb5004SJuraj Linkeš
144b6eb5004SJuraj Linkeš        Args:
145b6eb5004SJuraj Linkeš            test_case_sublist: Test case names to filter from this class.
146b6eb5004SJuraj Linkeš                If empty or :data:`None`, return all test cases.
147b6eb5004SJuraj Linkeš
148b6eb5004SJuraj Linkeš        Returns:
149b6eb5004SJuraj Linkeš            The filtered test case functions. This method returns functions as opposed to methods,
150b6eb5004SJuraj Linkeš            as methods are bound to instances and this method only has access to the class.
151b6eb5004SJuraj Linkeš
152b6eb5004SJuraj Linkeš        Raises:
153c64af3c7SLuca Vizzarro            ConfigurationError: If a test case from `test_case_sublist` is not found.
154b6eb5004SJuraj Linkeš        """
155b6eb5004SJuraj Linkeš        if test_case_sublist is None:
156b6eb5004SJuraj Linkeš            test_case_sublist = []
157b6eb5004SJuraj Linkeš
158b6eb5004SJuraj Linkeš        # the copy is needed so that the condition "elif test_case_sublist" doesn't
159b6eb5004SJuraj Linkeš        # change mid-cycle
160b6eb5004SJuraj Linkeš        test_case_sublist_copy = list(test_case_sublist)
161b6eb5004SJuraj Linkeš        func_test_cases = set()
162b6eb5004SJuraj Linkeš        perf_test_cases = set()
163b6eb5004SJuraj Linkeš
164c64af3c7SLuca Vizzarro        for test_case in cls.get_test_cases():
165c64af3c7SLuca Vizzarro            if test_case.name in test_case_sublist_copy:
166b6eb5004SJuraj Linkeš                # if test_case_sublist_copy is non-empty, remove the found test case
167b6eb5004SJuraj Linkeš                # so that we can look at the remainder at the end
168c64af3c7SLuca Vizzarro                test_case_sublist_copy.remove(test_case.name)
169b6eb5004SJuraj Linkeš            elif test_case_sublist:
170b6eb5004SJuraj Linkeš                # the original list not being empty means we're filtering test cases
171c64af3c7SLuca Vizzarro                # since we didn't remove test_case.name in the previous branch,
172b6eb5004SJuraj Linkeš                # it doesn't match the filter and we don't want to remove it
173b6eb5004SJuraj Linkeš                continue
174b6eb5004SJuraj Linkeš
175c64af3c7SLuca Vizzarro            match test_case.test_type:
176b6eb5004SJuraj Linkeš                case TestCaseType.PERFORMANCE:
177c64af3c7SLuca Vizzarro                    perf_test_cases.add(test_case)
178b6eb5004SJuraj Linkeš                case TestCaseType.FUNCTIONAL:
179c64af3c7SLuca Vizzarro                    func_test_cases.add(test_case)
180b6eb5004SJuraj Linkeš
181b6eb5004SJuraj Linkeš        if test_case_sublist_copy:
182b6eb5004SJuraj Linkeš            raise ConfigurationError(
183b6eb5004SJuraj Linkeš                f"Test cases {test_case_sublist_copy} not found among functions of {cls.__name__}."
184b6eb5004SJuraj Linkeš            )
185b6eb5004SJuraj Linkeš
186b6eb5004SJuraj Linkeš        return func_test_cases, perf_test_cases
187b6eb5004SJuraj Linkeš
1886fc05ca7SJuraj Linkeš    def set_up_suite(self) -> None:
1896ef07151SJuraj Linkeš        """Set up test fixtures common to all test cases.
1906ef07151SJuraj Linkeš
1916ef07151SJuraj Linkeš        This is done before any test case has been run.
1926fc05ca7SJuraj Linkeš        """
1936fc05ca7SJuraj Linkeš
1946fc05ca7SJuraj Linkeš    def tear_down_suite(self) -> None:
1956ef07151SJuraj Linkeš        """Tear down the previously created test fixtures common to all test cases.
1966ef07151SJuraj Linkeš
1976ef07151SJuraj Linkeš        This is done after all test have been run.
1986fc05ca7SJuraj Linkeš        """
1996fc05ca7SJuraj Linkeš
2006fc05ca7SJuraj Linkeš    def set_up_test_case(self) -> None:
2016ef07151SJuraj Linkeš        """Set up test fixtures before each test case.
2026ef07151SJuraj Linkeš
2036ef07151SJuraj Linkeš        This is done before *each* test case.
2046fc05ca7SJuraj Linkeš        """
2056fc05ca7SJuraj Linkeš
2066fc05ca7SJuraj Linkeš    def tear_down_test_case(self) -> None:
2076ef07151SJuraj Linkeš        """Tear down the previously created test fixtures after each test case.
2086ef07151SJuraj Linkeš
2096ef07151SJuraj Linkeš        This is done after *each* test case.
2106fc05ca7SJuraj Linkeš        """
2116fc05ca7SJuraj Linkeš
212bad934bfSJeremy Spewock    def send_packet_and_capture(
213bad934bfSJeremy Spewock        self,
214bad934bfSJeremy Spewock        packet: Packet,
215bad934bfSJeremy Spewock        filter_config: PacketFilteringConfig = PacketFilteringConfig(),
216bad934bfSJeremy Spewock        duration: float = 1,
217bad934bfSJeremy Spewock    ) -> list[Packet]:
2186ef07151SJuraj Linkeš        """Send and receive `packet` using the associated TG.
2196ef07151SJuraj Linkeš
2206ef07151SJuraj Linkeš        Send `packet` through the appropriate interface and receive on the appropriate interface.
2216ef07151SJuraj Linkeš        Modify the packet with l3/l2 addresses corresponding to the testbed and desired traffic.
2226ef07151SJuraj Linkeš
2236ef07151SJuraj Linkeš        Args:
2246ef07151SJuraj Linkeš            packet: The packet to send.
225bad934bfSJeremy Spewock            filter_config: The filter to use when capturing packets.
2266ef07151SJuraj Linkeš            duration: Capture traffic for this amount of time after sending `packet`.
2276ef07151SJuraj Linkeš
2286ef07151SJuraj Linkeš        Returns:
2296ef07151SJuraj Linkeš            A list of received packets.
230d99250aaSJuraj Linkeš        """
2310bf6796aSLuca Vizzarro        return self.send_packets_and_capture(
2320bf6796aSLuca Vizzarro            [packet],
2330bf6796aSLuca Vizzarro            filter_config,
2340bf6796aSLuca Vizzarro            duration,
2350bf6796aSLuca Vizzarro        )
2360bf6796aSLuca Vizzarro
2370bf6796aSLuca Vizzarro    def send_packets_and_capture(
2380bf6796aSLuca Vizzarro        self,
2390bf6796aSLuca Vizzarro        packets: list[Packet],
2400bf6796aSLuca Vizzarro        filter_config: PacketFilteringConfig = PacketFilteringConfig(),
2410bf6796aSLuca Vizzarro        duration: float = 1,
2420bf6796aSLuca Vizzarro    ) -> list[Packet]:
2430bf6796aSLuca Vizzarro        """Send and receive `packets` using the associated TG.
2440bf6796aSLuca Vizzarro
2450bf6796aSLuca Vizzarro        Send `packets` through the appropriate interface and receive on the appropriate interface.
2460bf6796aSLuca Vizzarro        Modify the packets with l3/l2 addresses corresponding to the testbed and desired traffic.
2470bf6796aSLuca Vizzarro
2480bf6796aSLuca Vizzarro        Args:
2490bf6796aSLuca Vizzarro            packets: The packets to send.
2500bf6796aSLuca Vizzarro            filter_config: The filter to use when capturing packets.
2510bf6796aSLuca Vizzarro            duration: Capture traffic for this amount of time after sending `packet`.
2520bf6796aSLuca Vizzarro
2530bf6796aSLuca Vizzarro        Returns:
2540bf6796aSLuca Vizzarro            A list of received packets.
2550bf6796aSLuca Vizzarro        """
2561a182596SJeremy Spewock        packets = self._adjust_addresses(packets)
2570bf6796aSLuca Vizzarro        return self.tg_node.send_packets_and_capture(
2580bf6796aSLuca Vizzarro            packets,
259bad934bfSJeremy Spewock            self._tg_port_egress,
260bad934bfSJeremy Spewock            self._tg_port_ingress,
261bad934bfSJeremy Spewock            filter_config,
262bad934bfSJeremy Spewock            duration,
263d99250aaSJuraj Linkeš        )
264d99250aaSJuraj Linkeš
2652e69387aSJeremy Spewock    def send_packets(
2662e69387aSJeremy Spewock        self,
2672e69387aSJeremy Spewock        packets: list[Packet],
2682e69387aSJeremy Spewock    ) -> None:
2692e69387aSJeremy Spewock        """Send packets using the traffic generator and do not capture received traffic.
2702e69387aSJeremy Spewock
2712e69387aSJeremy Spewock        Args:
2722e69387aSJeremy Spewock            packets: Packets to send.
2732e69387aSJeremy Spewock        """
2742e69387aSJeremy Spewock        packets = self._adjust_addresses(packets)
2752e69387aSJeremy Spewock        self.tg_node.send_packets(packets, self._tg_port_egress)
2762e69387aSJeremy Spewock
277*8d46662dSLuca Vizzarro    def get_expected_packets(self, packets: list[Packet]) -> list[Packet]:
278*8d46662dSLuca Vizzarro        """Inject the proper L2/L3 addresses into `packets`.
279*8d46662dSLuca Vizzarro
280*8d46662dSLuca Vizzarro        Inject the L2/L3 addresses expected at the receiving end of the traffic generator.
281*8d46662dSLuca Vizzarro
282*8d46662dSLuca Vizzarro        Args:
283*8d46662dSLuca Vizzarro            packets: The packets to modify.
284*8d46662dSLuca Vizzarro
285*8d46662dSLuca Vizzarro        Returns:
286*8d46662dSLuca Vizzarro            `packets` with injected L2/L3 addresses.
287*8d46662dSLuca Vizzarro        """
288*8d46662dSLuca Vizzarro        return self._adjust_addresses(packets, expected=True)
289*8d46662dSLuca Vizzarro
290d99250aaSJuraj Linkeš    def get_expected_packet(self, packet: Packet) -> Packet:
2916ef07151SJuraj Linkeš        """Inject the proper L2/L3 addresses into `packet`.
2926ef07151SJuraj Linkeš
293*8d46662dSLuca Vizzarro        Inject the L2/L3 addresses expected at the receiving end of the traffic generator.
294*8d46662dSLuca Vizzarro
2956ef07151SJuraj Linkeš        Args:
2966ef07151SJuraj Linkeš            packet: The packet to modify.
2976ef07151SJuraj Linkeš
2986ef07151SJuraj Linkeš        Returns:
2996ef07151SJuraj Linkeš            `packet` with injected L2/L3 addresses.
3006ef07151SJuraj Linkeš        """
301*8d46662dSLuca Vizzarro        return self.get_expected_packets([packet])[0]
302d99250aaSJuraj Linkeš
3031a182596SJeremy Spewock    def _adjust_addresses(self, packets: list[Packet], expected: bool = False) -> list[Packet]:
3046ef07151SJuraj Linkeš        """L2 and L3 address additions in both directions.
3056ef07151SJuraj Linkeš
306a97f8cc4SLuca Vizzarro        Copies of `packets` will be made, modified and returned in this method.
3071a182596SJeremy Spewock
3081a182596SJeremy Spewock        Only missing addresses are added to packets, existing addresses will not be overridden. If
3091a182596SJeremy Spewock        any packet in `packets` has multiple IP layers (using GRE, for example) only the inner-most
3101a182596SJeremy Spewock        IP layer will have its addresses adjusted.
3111a182596SJeremy Spewock
312d99250aaSJuraj Linkeš        Assumptions:
3136ef07151SJuraj Linkeš            Two links between SUT and TG, one link is TG -> SUT, the other SUT -> TG.
3146ef07151SJuraj Linkeš
3156ef07151SJuraj Linkeš        Args:
3161a182596SJeremy Spewock            packets: The packets to modify.
3176ef07151SJuraj Linkeš            expected: If :data:`True`, the direction is SUT -> TG,
3186ef07151SJuraj Linkeš                otherwise the direction is TG -> SUT.
3191a182596SJeremy Spewock
3201a182596SJeremy Spewock        Returns:
3211a182596SJeremy Spewock            A list containing copies of all packets in `packets` after modification.
322d99250aaSJuraj Linkeš        """
3231a182596SJeremy Spewock        ret_packets = []
324a97f8cc4SLuca Vizzarro        for original_packet in packets:
325a97f8cc4SLuca Vizzarro            packet = original_packet.copy()
326a97f8cc4SLuca Vizzarro
327d99250aaSJuraj Linkeš            # update l2 addresses
3281a182596SJeremy Spewock            # If `expected` is :data:`True`, the packet enters the TG from SUT, otherwise the
3291a182596SJeremy Spewock            # packet leaves the TG towards the SUT.
330d99250aaSJuraj Linkeš
3311a182596SJeremy Spewock            # The fields parameter of a packet does not include fields of the payload, so this can
3321a182596SJeremy Spewock            # only be the Ether src/dst.
3331a182596SJeremy Spewock            if "src" not in packet.fields:
3341a182596SJeremy Spewock                packet.src = (
3351a182596SJeremy Spewock                    self._sut_port_egress.mac_address
3361a182596SJeremy Spewock                    if expected
3371a182596SJeremy Spewock                    else self._tg_port_egress.mac_address
3381a182596SJeremy Spewock                )
3391a182596SJeremy Spewock            if "dst" not in packet.fields:
3401a182596SJeremy Spewock                packet.dst = (
3411a182596SJeremy Spewock                    self._tg_port_ingress.mac_address
3421a182596SJeremy Spewock                    if expected
3431a182596SJeremy Spewock                    else self._sut_port_ingress.mac_address
3441a182596SJeremy Spewock                )
3451a182596SJeremy Spewock
346d99250aaSJuraj Linkeš            # update l3 addresses
3471a182596SJeremy Spewock            # The packet is routed from TG egress to TG ingress regardless of whether it is
3481a182596SJeremy Spewock            # expected or not.
3491a182596SJeremy Spewock            num_ip_layers = packet.layers().count(IP)
3501a182596SJeremy Spewock            if num_ip_layers > 0:
3511a182596SJeremy Spewock                # Update the last IP layer if there are multiple (the framework should be modifying
3521a182596SJeremy Spewock                # the packet address instead of the tunnel address if there is one).
3531a182596SJeremy Spewock                l3_to_use = packet.getlayer(IP, num_ip_layers)
3541a182596SJeremy Spewock                if "src" not in l3_to_use.fields:
3551a182596SJeremy Spewock                    l3_to_use.src = self._tg_ip_address_egress.ip.exploded
356d99250aaSJuraj Linkeš
3571a182596SJeremy Spewock                if "dst" not in l3_to_use.fields:
3581a182596SJeremy Spewock                    l3_to_use.dst = self._tg_ip_address_ingress.ip.exploded
3591a182596SJeremy Spewock            ret_packets.append(Ether(packet.build()))
360d99250aaSJuraj Linkeš
3611a182596SJeremy Spewock        return ret_packets
362d99250aaSJuraj Linkeš
3636fc05ca7SJuraj Linkeš    def verify(self, condition: bool, failure_description: str) -> None:
3646ef07151SJuraj Linkeš        """Verify `condition` and handle failures.
3656ef07151SJuraj Linkeš
3666ef07151SJuraj Linkeš        When `condition` is :data:`False`, raise an exception and log the last 10 commands
3676ef07151SJuraj Linkeš        executed on both the SUT and TG.
3686ef07151SJuraj Linkeš
3696ef07151SJuraj Linkeš        Args:
3706ef07151SJuraj Linkeš            condition: The condition to check.
3716ef07151SJuraj Linkeš            failure_description: A short description of the failure
3726ef07151SJuraj Linkeš                that will be stored in the raised exception.
3736ef07151SJuraj Linkeš
3746ef07151SJuraj Linkeš        Raises:
3756ef07151SJuraj Linkeš            TestCaseVerifyError: `condition` is :data:`False`.
3766ef07151SJuraj Linkeš        """
3776fc05ca7SJuraj Linkeš        if not condition:
378d99250aaSJuraj Linkeš            self._fail_test_case_verify(failure_description)
379d99250aaSJuraj Linkeš
380d99250aaSJuraj Linkeš    def _fail_test_case_verify(self, failure_description: str) -> None:
381517b4b26SJuraj Linkeš        self._logger.debug("A test case failed, showing the last 10 commands executed on SUT:")
3826fc05ca7SJuraj Linkeš        for command_res in self.sut_node.main_session.remote_session.history[-10:]:
3836fc05ca7SJuraj Linkeš            self._logger.debug(command_res.command)
384517b4b26SJuraj Linkeš        self._logger.debug("A test case failed, showing the last 10 commands executed on TG:")
385d99250aaSJuraj Linkeš        for command_res in self.tg_node.main_session.remote_session.history[-10:]:
386d99250aaSJuraj Linkeš            self._logger.debug(command_res.command)
3876fc05ca7SJuraj Linkeš        raise TestCaseVerifyError(failure_description)
3886fc05ca7SJuraj Linkeš
389517b4b26SJuraj Linkeš    def verify_packets(self, expected_packet: Packet, received_packets: list[Packet]) -> None:
3906ef07151SJuraj Linkeš        """Verify that `expected_packet` has been received.
3916ef07151SJuraj Linkeš
3926ef07151SJuraj Linkeš        Go through `received_packets` and check that `expected_packet` is among them.
3936ef07151SJuraj Linkeš        If not, raise an exception and log the last 10 commands
3946ef07151SJuraj Linkeš        executed on both the SUT and TG.
3956ef07151SJuraj Linkeš
3966ef07151SJuraj Linkeš        Args:
3976ef07151SJuraj Linkeš            expected_packet: The packet we're expecting to receive.
3986ef07151SJuraj Linkeš            received_packets: The packets where we're looking for `expected_packet`.
3996ef07151SJuraj Linkeš
4006ef07151SJuraj Linkeš        Raises:
4016ef07151SJuraj Linkeš            TestCaseVerifyError: `expected_packet` is not among `received_packets`.
4026ef07151SJuraj Linkeš        """
403d99250aaSJuraj Linkeš        for received_packet in received_packets:
404d99250aaSJuraj Linkeš            if self._compare_packets(expected_packet, received_packet):
405d99250aaSJuraj Linkeš                break
406d99250aaSJuraj Linkeš        else:
407d99250aaSJuraj Linkeš            self._logger.debug(
408d99250aaSJuraj Linkeš                f"The expected packet {get_packet_summaries(expected_packet)} "
409d99250aaSJuraj Linkeš                f"not found among received {get_packet_summaries(received_packets)}"
410d99250aaSJuraj Linkeš            )
411517b4b26SJuraj Linkeš            self._fail_test_case_verify("An expected packet not found among received packets.")
412d99250aaSJuraj Linkeš
4130bf6796aSLuca Vizzarro    def match_all_packets(
4140bf6796aSLuca Vizzarro        self, expected_packets: list[Packet], received_packets: list[Packet]
4150bf6796aSLuca Vizzarro    ) -> None:
4160bf6796aSLuca Vizzarro        """Matches all the expected packets against the received ones.
4170bf6796aSLuca Vizzarro
4180bf6796aSLuca Vizzarro        Matching is performed by counting down the occurrences in a dictionary which keys are the
4190bf6796aSLuca Vizzarro        raw packet bytes. No deep packet comparison is performed. All the unexpected packets (noise)
4200bf6796aSLuca Vizzarro        are automatically ignored.
4210bf6796aSLuca Vizzarro
4220bf6796aSLuca Vizzarro        Args:
4230bf6796aSLuca Vizzarro            expected_packets: The packets we are expecting to receive.
4240bf6796aSLuca Vizzarro            received_packets: All the packets that were received.
4250bf6796aSLuca Vizzarro
4260bf6796aSLuca Vizzarro        Raises:
4270bf6796aSLuca Vizzarro            TestCaseVerifyError: if and not all the `expected_packets` were found in
4280bf6796aSLuca Vizzarro                `received_packets`.
4290bf6796aSLuca Vizzarro        """
4300bf6796aSLuca Vizzarro        expected_packets_counters = Counter(map(raw, expected_packets))
4310bf6796aSLuca Vizzarro        received_packets_counters = Counter(map(raw, received_packets))
4320bf6796aSLuca Vizzarro        # The number of expected packets is subtracted by the number of received packets, ignoring
4330bf6796aSLuca Vizzarro        # any unexpected packets and capping at zero.
4340bf6796aSLuca Vizzarro        missing_packets_counters = expected_packets_counters - received_packets_counters
4350bf6796aSLuca Vizzarro        missing_packets_count = missing_packets_counters.total()
4360bf6796aSLuca Vizzarro        self._logger.debug(
4370bf6796aSLuca Vizzarro            f"match_all_packets: expected {len(expected_packets)}, "
4380bf6796aSLuca Vizzarro            f"received {len(received_packets)}, missing {missing_packets_count}"
4390bf6796aSLuca Vizzarro        )
4400bf6796aSLuca Vizzarro
4410bf6796aSLuca Vizzarro        if missing_packets_count != 0:
4420bf6796aSLuca Vizzarro            self._fail_test_case_verify(
4430bf6796aSLuca Vizzarro                f"Not all packets were received, expected {len(expected_packets)} "
4440bf6796aSLuca Vizzarro                f"but {missing_packets_count} were missing."
4450bf6796aSLuca Vizzarro            )
4460bf6796aSLuca Vizzarro
447517b4b26SJuraj Linkeš    def _compare_packets(self, expected_packet: Packet, received_packet: Packet) -> bool:
448d99250aaSJuraj Linkeš        self._logger.debug(
449517b4b26SJuraj Linkeš            f"Comparing packets: \n{expected_packet.summary()}\n{received_packet.summary()}"
450d99250aaSJuraj Linkeš        )
451d99250aaSJuraj Linkeš
452d99250aaSJuraj Linkeš        l3 = IP in expected_packet.layers()
453d99250aaSJuraj Linkeš        self._logger.debug("Found l3 layer")
454d99250aaSJuraj Linkeš
455d99250aaSJuraj Linkeš        received_payload = received_packet
456d99250aaSJuraj Linkeš        expected_payload = expected_packet
457d99250aaSJuraj Linkeš        while received_payload and expected_payload:
458d99250aaSJuraj Linkeš            self._logger.debug("Comparing payloads:")
459d99250aaSJuraj Linkeš            self._logger.debug(f"Received: {received_payload}")
460d99250aaSJuraj Linkeš            self._logger.debug(f"Expected: {expected_payload}")
461d99250aaSJuraj Linkeš            if received_payload.__class__ == expected_payload.__class__:
462d99250aaSJuraj Linkeš                self._logger.debug("The layers are the same.")
463d99250aaSJuraj Linkeš                if received_payload.__class__ == Ether:
464d99250aaSJuraj Linkeš                    if not self._verify_l2_frame(received_payload, l3):
465d99250aaSJuraj Linkeš                        return False
466d99250aaSJuraj Linkeš                elif received_payload.__class__ == IP:
467d99250aaSJuraj Linkeš                    if not self._verify_l3_packet(received_payload, expected_payload):
468d99250aaSJuraj Linkeš                        return False
469d99250aaSJuraj Linkeš            else:
470d99250aaSJuraj Linkeš                # Different layers => different packets
471d99250aaSJuraj Linkeš                return False
472d99250aaSJuraj Linkeš            received_payload = received_payload.payload
473d99250aaSJuraj Linkeš            expected_payload = expected_payload.payload
474d99250aaSJuraj Linkeš
475d99250aaSJuraj Linkeš        if expected_payload:
476517b4b26SJuraj Linkeš            self._logger.debug(f"The expected packet did not contain {expected_payload}.")
477d99250aaSJuraj Linkeš            return False
478d99250aaSJuraj Linkeš        if received_payload and received_payload.__class__ != Padding:
479517b4b26SJuraj Linkeš            self._logger.debug("The received payload had extra layers which were not padding.")
480d99250aaSJuraj Linkeš            return False
481d99250aaSJuraj Linkeš        return True
482d99250aaSJuraj Linkeš
483d99250aaSJuraj Linkeš    def _verify_l2_frame(self, received_packet: Ether, l3: bool) -> bool:
484d99250aaSJuraj Linkeš        self._logger.debug("Looking at the Ether layer.")
485d99250aaSJuraj Linkeš        self._logger.debug(
486d99250aaSJuraj Linkeš            f"Comparing received dst mac '{received_packet.dst}' "
487d99250aaSJuraj Linkeš            f"with expected '{self._tg_port_ingress.mac_address}'."
488d99250aaSJuraj Linkeš        )
489d99250aaSJuraj Linkeš        if received_packet.dst != self._tg_port_ingress.mac_address:
490d99250aaSJuraj Linkeš            return False
491d99250aaSJuraj Linkeš
492d99250aaSJuraj Linkeš        expected_src_mac = self._tg_port_egress.mac_address
493d99250aaSJuraj Linkeš        if l3:
494d99250aaSJuraj Linkeš            expected_src_mac = self._sut_port_egress.mac_address
495d99250aaSJuraj Linkeš        self._logger.debug(
496d99250aaSJuraj Linkeš            f"Comparing received src mac '{received_packet.src}' "
497d99250aaSJuraj Linkeš            f"with expected '{expected_src_mac}'."
498d99250aaSJuraj Linkeš        )
499d99250aaSJuraj Linkeš        if received_packet.src != expected_src_mac:
500d99250aaSJuraj Linkeš            return False
501d99250aaSJuraj Linkeš
502d99250aaSJuraj Linkeš        return True
503d99250aaSJuraj Linkeš
504d99250aaSJuraj Linkeš    def _verify_l3_packet(self, received_packet: IP, expected_packet: IP) -> bool:
505d99250aaSJuraj Linkeš        self._logger.debug("Looking at the IP layer.")
506517b4b26SJuraj Linkeš        if received_packet.src != expected_packet.src or received_packet.dst != expected_packet.dst:
507d99250aaSJuraj Linkeš            return False
508d99250aaSJuraj Linkeš        return True
509b6eb5004SJuraj Linkeš
510b6eb5004SJuraj Linkeš
511b6eb5004SJuraj Linkeš#: The generic type for a method of an instance of TestSuite
512b6eb5004SJuraj LinkešTestSuiteMethodType = TypeVar("TestSuiteMethodType", bound=Callable[[TestSuite], None])
513b6eb5004SJuraj Linkeš
514b6eb5004SJuraj Linkeš
515b6eb5004SJuraj Linkešclass TestCaseType(Enum):
516b6eb5004SJuraj Linkeš    """The types of test cases."""
517b6eb5004SJuraj Linkeš
518b6eb5004SJuraj Linkeš    #:
519b6eb5004SJuraj Linkeš    FUNCTIONAL = auto()
520b6eb5004SJuraj Linkeš    #:
521b6eb5004SJuraj Linkeš    PERFORMANCE = auto()
522b6eb5004SJuraj Linkeš
523b6eb5004SJuraj Linkeš
524566201aeSJuraj Linkešclass TestCase(TestProtocol, Protocol[TestSuiteMethodType]):
525b6eb5004SJuraj Linkeš    """Definition of the test case type for static type checking purposes.
526b6eb5004SJuraj Linkeš
527b6eb5004SJuraj Linkeš    The type is applied to test case functions through a decorator, which casts the decorated
528b6eb5004SJuraj Linkeš    test case function to :class:`TestCase` and sets common variables.
529b6eb5004SJuraj Linkeš    """
530b6eb5004SJuraj Linkeš
531b6eb5004SJuraj Linkeš    #:
532c64af3c7SLuca Vizzarro    name: ClassVar[str]
533c64af3c7SLuca Vizzarro    #:
534b6eb5004SJuraj Linkeš    test_type: ClassVar[TestCaseType]
535b6eb5004SJuraj Linkeš    #: necessary for mypy so that it can treat this class as the function it's shadowing
536b6eb5004SJuraj Linkeš    __call__: TestSuiteMethodType
537b6eb5004SJuraj Linkeš
538b6eb5004SJuraj Linkeš    @classmethod
539b6eb5004SJuraj Linkeš    def make_decorator(
540b6eb5004SJuraj Linkeš        cls, test_case_type: TestCaseType
541b6eb5004SJuraj Linkeš    ) -> Callable[[TestSuiteMethodType], type["TestCase"]]:
542b6eb5004SJuraj Linkeš        """Create a decorator for test suites.
543b6eb5004SJuraj Linkeš
544b6eb5004SJuraj Linkeš        The decorator casts the decorated function as :class:`TestCase`,
545b6eb5004SJuraj Linkeš        sets it as `test_case_type`
546b6eb5004SJuraj Linkeš        and initializes common variables defined in :class:`RequiresCapabilities`.
547b6eb5004SJuraj Linkeš
548b6eb5004SJuraj Linkeš        Args:
549b6eb5004SJuraj Linkeš            test_case_type: Either a functional or performance test case.
550b6eb5004SJuraj Linkeš
551b6eb5004SJuraj Linkeš        Returns:
552b6eb5004SJuraj Linkeš            The decorator of a functional or performance test case.
553b6eb5004SJuraj Linkeš        """
554b6eb5004SJuraj Linkeš
555b6eb5004SJuraj Linkeš        def _decorator(func: TestSuiteMethodType) -> type[TestCase]:
556b6eb5004SJuraj Linkeš            test_case = cast(type[TestCase], func)
557c64af3c7SLuca Vizzarro            test_case.name = func.__name__
558566201aeSJuraj Linkeš            test_case.skip = cls.skip
559566201aeSJuraj Linkeš            test_case.skip_reason = cls.skip_reason
560eebfb5bbSJuraj Linkeš            test_case.required_capabilities = set()
561039256daSJuraj Linkeš            test_case.topology_type = cls.topology_type
562039256daSJuraj Linkeš            test_case.topology_type.add_to_required(test_case)
563b6eb5004SJuraj Linkeš            test_case.test_type = test_case_type
564b6eb5004SJuraj Linkeš            return test_case
565b6eb5004SJuraj Linkeš
566b6eb5004SJuraj Linkeš        return _decorator
567b6eb5004SJuraj Linkeš
568b6eb5004SJuraj Linkeš
569b6eb5004SJuraj Linkeš#: The decorator for functional test cases.
570b6eb5004SJuraj Linkešfunc_test: Callable = TestCase.make_decorator(TestCaseType.FUNCTIONAL)
571b6eb5004SJuraj Linkeš#: The decorator for performance test cases.
572b6eb5004SJuraj Linkešperf_test: Callable = TestCase.make_decorator(TestCaseType.PERFORMANCE)
573c64af3c7SLuca Vizzarro
574c64af3c7SLuca Vizzarro
575c64af3c7SLuca Vizzarro@dataclass
576c64af3c7SLuca Vizzarroclass TestSuiteSpec:
577c64af3c7SLuca Vizzarro    """A class defining the specification of a test suite.
578c64af3c7SLuca Vizzarro
579c64af3c7SLuca Vizzarro    Apart from defining all the specs of a test suite, a helper function :meth:`discover_all` is
580c64af3c7SLuca Vizzarro    provided to automatically discover all the available test suites.
581c64af3c7SLuca Vizzarro
582c64af3c7SLuca Vizzarro    Attributes:
583c64af3c7SLuca Vizzarro        module_name: The name of the test suite's module.
584c64af3c7SLuca Vizzarro    """
585c64af3c7SLuca Vizzarro
586c64af3c7SLuca Vizzarro    #:
587c64af3c7SLuca Vizzarro    TEST_SUITES_PACKAGE_NAME = "tests"
588c64af3c7SLuca Vizzarro    #:
589c64af3c7SLuca Vizzarro    TEST_SUITE_MODULE_PREFIX = "TestSuite_"
590c64af3c7SLuca Vizzarro    #:
591c64af3c7SLuca Vizzarro    TEST_SUITE_CLASS_PREFIX = "Test"
592c64af3c7SLuca Vizzarro    #:
593c64af3c7SLuca Vizzarro    TEST_CASE_METHOD_PREFIX = "test_"
594c64af3c7SLuca Vizzarro    #:
595c64af3c7SLuca Vizzarro    FUNC_TEST_CASE_REGEX = r"test_(?!perf_)"
596c64af3c7SLuca Vizzarro    #:
597c64af3c7SLuca Vizzarro    PERF_TEST_CASE_REGEX = r"test_perf_"
598c64af3c7SLuca Vizzarro
599c64af3c7SLuca Vizzarro    module_name: str
600c64af3c7SLuca Vizzarro
601c64af3c7SLuca Vizzarro    @cached_property
602c64af3c7SLuca Vizzarro    def name(self) -> str:
603c64af3c7SLuca Vizzarro        """The name of the test suite's module."""
604c64af3c7SLuca Vizzarro        return self.module_name[len(self.TEST_SUITE_MODULE_PREFIX) :]
605c64af3c7SLuca Vizzarro
606c64af3c7SLuca Vizzarro    @cached_property
607c64af3c7SLuca Vizzarro    def module(self) -> ModuleType:
608c64af3c7SLuca Vizzarro        """A reference to the test suite's module."""
609c64af3c7SLuca Vizzarro        return import_module(f"{self.TEST_SUITES_PACKAGE_NAME}.{self.module_name}")
610c64af3c7SLuca Vizzarro
611c64af3c7SLuca Vizzarro    @cached_property
612c64af3c7SLuca Vizzarro    def class_name(self) -> str:
613c64af3c7SLuca Vizzarro        """The name of the test suite's class."""
614c64af3c7SLuca Vizzarro        return f"{self.TEST_SUITE_CLASS_PREFIX}{to_pascal_case(self.name)}"
615c64af3c7SLuca Vizzarro
616c64af3c7SLuca Vizzarro    @cached_property
617c64af3c7SLuca Vizzarro    def class_obj(self) -> type[TestSuite]:
618c64af3c7SLuca Vizzarro        """A reference to the test suite's class."""
619c64af3c7SLuca Vizzarro
620c64af3c7SLuca Vizzarro        def is_test_suite(obj) -> bool:
621c64af3c7SLuca Vizzarro            """Check whether `obj` is a :class:`TestSuite`.
622c64af3c7SLuca Vizzarro
623c64af3c7SLuca Vizzarro            The `obj` is a subclass of :class:`TestSuite`, but not :class:`TestSuite` itself.
624c64af3c7SLuca Vizzarro
625c64af3c7SLuca Vizzarro            Args:
626c64af3c7SLuca Vizzarro                obj: The object to be checked.
627c64af3c7SLuca Vizzarro
628c64af3c7SLuca Vizzarro            Returns:
629c64af3c7SLuca Vizzarro                :data:`True` if `obj` is a subclass of `TestSuite`.
630c64af3c7SLuca Vizzarro            """
631c64af3c7SLuca Vizzarro            try:
632c64af3c7SLuca Vizzarro                if issubclass(obj, TestSuite) and obj is not TestSuite:
633c64af3c7SLuca Vizzarro                    return True
634c64af3c7SLuca Vizzarro            except TypeError:
635c64af3c7SLuca Vizzarro                return False
636c64af3c7SLuca Vizzarro            return False
637c64af3c7SLuca Vizzarro
638c64af3c7SLuca Vizzarro        for class_name, class_obj in inspect.getmembers(self.module, is_test_suite):
639c64af3c7SLuca Vizzarro            if class_name == self.class_name:
640c64af3c7SLuca Vizzarro                return class_obj
641c64af3c7SLuca Vizzarro
642c64af3c7SLuca Vizzarro        raise InternalError(
643c64af3c7SLuca Vizzarro            f"Expected class {self.class_name} not found in module {self.module_name}."
644c64af3c7SLuca Vizzarro        )
645c64af3c7SLuca Vizzarro
646c64af3c7SLuca Vizzarro    @classmethod
647c64af3c7SLuca Vizzarro    def discover_all(
648c64af3c7SLuca Vizzarro        cls, package_name: str | None = None, module_prefix: str | None = None
649c64af3c7SLuca Vizzarro    ) -> list[Self]:
650c64af3c7SLuca Vizzarro        """Discover all the test suites.
651c64af3c7SLuca Vizzarro
652c64af3c7SLuca Vizzarro        The test suites are discovered in the provided `package_name`. The full module name,
653c64af3c7SLuca Vizzarro        expected under that package, is prefixed with `module_prefix`.
654c64af3c7SLuca Vizzarro        The module name is a standard filename with words separated with underscores.
655c64af3c7SLuca Vizzarro        For each module found, search for a :class:`TestSuite` class which starts
656c64af3c7SLuca Vizzarro        with :attr:`~TestSuiteSpec.TEST_SUITE_CLASS_PREFIX`, continuing with the module name in
657c64af3c7SLuca Vizzarro        PascalCase.
658c64af3c7SLuca Vizzarro
659c64af3c7SLuca Vizzarro        The PascalCase convention applies to abbreviations, acronyms, initialisms and so on::
660c64af3c7SLuca Vizzarro
661c64af3c7SLuca Vizzarro            OS -> Os
662c64af3c7SLuca Vizzarro            TCP -> Tcp
663c64af3c7SLuca Vizzarro
664c64af3c7SLuca Vizzarro        Args:
665c64af3c7SLuca Vizzarro            package_name: The name of the package where to find the test suites. If :data:`None`,
666c64af3c7SLuca Vizzarro                the :attr:`~TestSuiteSpec.TEST_SUITES_PACKAGE_NAME` is used.
667c64af3c7SLuca Vizzarro            module_prefix: The name prefix defining the test suite module. If :data:`None`, the
668c64af3c7SLuca Vizzarro                :attr:`~TestSuiteSpec.TEST_SUITE_MODULE_PREFIX` constant is used.
669c64af3c7SLuca Vizzarro
670c64af3c7SLuca Vizzarro        Returns:
671c64af3c7SLuca Vizzarro            A list containing all the discovered test suites.
672c64af3c7SLuca Vizzarro        """
673c64af3c7SLuca Vizzarro        if package_name is None:
674c64af3c7SLuca Vizzarro            package_name = cls.TEST_SUITES_PACKAGE_NAME
675c64af3c7SLuca Vizzarro        if module_prefix is None:
676c64af3c7SLuca Vizzarro            module_prefix = cls.TEST_SUITE_MODULE_PREFIX
677c64af3c7SLuca Vizzarro
678c64af3c7SLuca Vizzarro        test_suites = []
679c64af3c7SLuca Vizzarro
680c64af3c7SLuca Vizzarro        test_suites_pkg = import_module(package_name)
681c64af3c7SLuca Vizzarro        for _, module_name, is_pkg in iter_modules(test_suites_pkg.__path__):
682c64af3c7SLuca Vizzarro            if not module_name.startswith(module_prefix) or is_pkg:
683c64af3c7SLuca Vizzarro                continue
684c64af3c7SLuca Vizzarro
685c64af3c7SLuca Vizzarro            test_suite = cls(module_name)
686c64af3c7SLuca Vizzarro            try:
687c64af3c7SLuca Vizzarro                if test_suite.class_obj:
688c64af3c7SLuca Vizzarro                    test_suites.append(test_suite)
689c64af3c7SLuca Vizzarro            except InternalError as err:
690c64af3c7SLuca Vizzarro                get_dts_logger().warning(err)
691c64af3c7SLuca Vizzarro
692c64af3c7SLuca Vizzarro        return test_suites
693c64af3c7SLuca Vizzarro
694c64af3c7SLuca Vizzarro
695c64af3c7SLuca VizzarroAVAILABLE_TEST_SUITES: list[TestSuiteSpec] = TestSuiteSpec.discover_all()
696c64af3c7SLuca Vizzarro"""Constant to store all the available, discovered and imported test suites.
697c64af3c7SLuca Vizzarro
698c64af3c7SLuca VizzarroThe test suites should be gathered from this list to avoid importing more than once.
699c64af3c7SLuca Vizzarro"""
700c64af3c7SLuca Vizzarro
701c64af3c7SLuca Vizzarro
702c64af3c7SLuca Vizzarrodef find_by_name(name: str) -> TestSuiteSpec | None:
703c64af3c7SLuca Vizzarro    """Find a requested test suite by name from the available ones."""
704c64af3c7SLuca Vizzarro    test_suites = filter(lambda t: t.name == name, AVAILABLE_TEST_SUITES)
705c64af3c7SLuca Vizzarro    return next(test_suites, None)
706