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