1# SPDX-License-Identifier: BSD-3-Clause 2# Copyright(c) 2024 PANTHEON.tech s.r.o. 3 4"""Testbed capabilities. 5 6This module provides a protocol that defines the common attributes of test cases and suites 7and support for test environment capabilities. 8 9Many test cases are testing features not available on all hardware. 10On the other hand, some test cases or suites may not need the most complex topology available. 11 12The module allows developers to mark test cases or suites a requiring certain hardware capabilities 13or a particular topology with the :func:`requires` decorator. 14 15There are differences between hardware and topology capabilities: 16 17 * Hardware capabilities are assumed to not be required when not specified. 18 * However, some topology is always available, so each test case or suite is assigned 19 a default topology if no topology is specified in the decorator. 20 21The module also allows developers to mark test cases or suites as requiring certain 22hardware capabilities with the :func:`requires` decorator. 23 24Examples: 25 .. code:: python 26 27 from framework.test_suite import TestSuite, func_test 28 from framework.testbed_model.capability import TopologyType, requires 29 # The whole test suite (each test case within) doesn't require any links. 30 @requires(topology_type=TopologyType.no_link) 31 @func_test 32 class TestHelloWorld(TestSuite): 33 def hello_world_single_core(self): 34 ... 35 36 .. code:: python 37 38 from framework.test_suite import TestSuite, func_test 39 from framework.testbed_model.capability import NicCapability, requires 40 class TestPmdBufferScatter(TestSuite): 41 # only the test case requires the SCATTERED_RX_ENABLED capability 42 # other test cases may not require it 43 @requires(NicCapability.SCATTERED_RX_ENABLED) 44 @func_test 45 def test_scatter_mbuf_2048(self): 46""" 47 48import inspect 49from abc import ABC, abstractmethod 50from collections.abc import MutableSet, Sequence 51from dataclasses import dataclass 52from typing import Callable, ClassVar, Protocol 53 54from typing_extensions import Self 55 56from framework.exception import ConfigurationError 57from framework.logger import get_dts_logger 58from framework.remote_session.testpmd_shell import ( 59 NicCapability, 60 TestPmdShell, 61 TestPmdShellCapabilityMethod, 62 TestPmdShellDecorator, 63 TestPmdShellMethod, 64) 65 66from .sut_node import SutNode 67from .topology import Topology, TopologyType 68 69 70class Capability(ABC): 71 """The base class for various capabilities. 72 73 The same capability should always be represented by the same object, 74 meaning the same capability required by different test cases or suites 75 should point to the same object. 76 77 Example: 78 ``test_case1`` and ``test_case2`` each require ``capability1`` 79 and in both instances, ``capability1`` should point to the same capability object. 80 81 It is up to the subclasses how they implement this. 82 83 The instances are used in sets so they must be hashable. 84 """ 85 86 #: A set storing the capabilities whose support should be checked. 87 capabilities_to_check: ClassVar[set[Self]] = set() 88 89 def register_to_check(self) -> Callable[[SutNode, "Topology"], set[Self]]: 90 """Register the capability to be checked for support. 91 92 Returns: 93 The callback function that checks the support of capabilities of the particular subclass 94 which should be called after all capabilities have been registered. 95 """ 96 if not type(self).capabilities_to_check: 97 type(self).capabilities_to_check = set() 98 type(self).capabilities_to_check.add(self) 99 return type(self)._get_and_reset 100 101 def add_to_required(self, test_case_or_suite: type["TestProtocol"]) -> None: 102 """Add the capability instance to the required test case or suite's capabilities. 103 104 Args: 105 test_case_or_suite: The test case or suite among whose required capabilities 106 to add this instance. 107 """ 108 if not test_case_or_suite.required_capabilities: 109 test_case_or_suite.required_capabilities = set() 110 self._preprocess_required(test_case_or_suite) 111 test_case_or_suite.required_capabilities.add(self) 112 113 def _preprocess_required(self, test_case_or_suite: type["TestProtocol"]) -> None: 114 """An optional method that modifies the required capabilities.""" 115 116 @classmethod 117 def _get_and_reset(cls, sut_node: SutNode, topology: "Topology") -> set[Self]: 118 """The callback method to be called after all capabilities have been registered. 119 120 Not only does this method check the support of capabilities, 121 but it also reset the internal set of registered capabilities 122 so that the "register, then get support" workflow works in subsequent test runs. 123 """ 124 supported_capabilities = cls.get_supported_capabilities(sut_node, topology) 125 cls.capabilities_to_check = set() 126 return supported_capabilities 127 128 @classmethod 129 @abstractmethod 130 def get_supported_capabilities(cls, sut_node: SutNode, topology: "Topology") -> set[Self]: 131 """Get the support status of each registered capability. 132 133 Each subclass must implement this method and return the subset of supported capabilities 134 of :attr:`capabilities_to_check`. 135 136 Args: 137 sut_node: The SUT node of the current test run. 138 topology: The topology of the current test run. 139 140 Returns: 141 The supported capabilities. 142 """ 143 144 @abstractmethod 145 def __hash__(self) -> int: 146 """The subclasses must be hashable so that they can be stored in sets.""" 147 148 149@dataclass 150class DecoratedNicCapability(Capability): 151 """A wrapper around :class:`~framework.remote_session.testpmd_shell.NicCapability`. 152 153 New instances should be created with the :meth:`create_unique` class method to ensure 154 there are no duplicate instances. 155 156 Attributes: 157 nic_capability: The NIC capability that defines each instance. 158 capability_fn: The capability retrieval function of `nic_capability`. 159 capability_decorator: The decorator function of `nic_capability`. 160 This function will wrap `capability_fn`. 161 """ 162 163 nic_capability: NicCapability 164 capability_fn: TestPmdShellCapabilityMethod 165 capability_decorator: TestPmdShellDecorator | None 166 _unique_capabilities: ClassVar[dict[NicCapability, Self]] = {} 167 168 @classmethod 169 def get_unique(cls, nic_capability: NicCapability) -> "DecoratedNicCapability": 170 """Get the capability uniquely identified by `nic_capability`. 171 172 This is a factory method that implements a quasi-enum pattern. 173 The instances of this class are stored in an internal class variable, 174 `_unique_capabilities`. 175 176 If an instance identified by `nic_capability` doesn't exist, 177 it is created and added to `_unique_capabilities`. 178 If it exists, it is returned so that a new identical instance is not created. 179 180 Args: 181 nic_capability: The NIC capability. 182 183 Returns: 184 The capability uniquely identified by `nic_capability`. 185 """ 186 decorator_fn = None 187 if isinstance(nic_capability.value, tuple): 188 capability_fn, decorator_fn = nic_capability.value 189 else: 190 capability_fn = nic_capability.value 191 192 if nic_capability not in cls._unique_capabilities: 193 cls._unique_capabilities[nic_capability] = cls( 194 nic_capability, capability_fn, decorator_fn 195 ) 196 return cls._unique_capabilities[nic_capability] 197 198 @classmethod 199 def get_supported_capabilities( 200 cls, sut_node: SutNode, topology: "Topology" 201 ) -> set["DecoratedNicCapability"]: 202 """Overrides :meth:`~Capability.get_supported_capabilities`. 203 204 The capabilities are first sorted by decorators, then reduced into a single function which 205 is then passed to the decorator. This way we execute each decorator only once. 206 Each capability is first checked whether it's supported/unsupported 207 before executing its `capability_fn` so that each capability is retrieved only once. 208 """ 209 supported_conditional_capabilities: set["DecoratedNicCapability"] = set() 210 logger = get_dts_logger(f"{sut_node.name}.{cls.__name__}") 211 if topology.type is topology.type.no_link: 212 logger.debug( 213 "No links available in the current topology, not getting NIC capabilities." 214 ) 215 return supported_conditional_capabilities 216 logger.debug( 217 f"Checking which NIC capabilities from {cls.capabilities_to_check} are supported." 218 ) 219 if cls.capabilities_to_check: 220 capabilities_to_check_map = cls._get_decorated_capabilities_map() 221 with TestPmdShell( 222 sut_node, privileged=True, disable_device_start=True 223 ) as testpmd_shell: 224 for conditional_capability_fn, capabilities in capabilities_to_check_map.items(): 225 supported_capabilities: set[NicCapability] = set() 226 unsupported_capabilities: set[NicCapability] = set() 227 capability_fn = cls._reduce_capabilities( 228 capabilities, supported_capabilities, unsupported_capabilities 229 ) 230 if conditional_capability_fn: 231 capability_fn = conditional_capability_fn(capability_fn) 232 capability_fn(testpmd_shell) 233 for capability in capabilities: 234 if capability.nic_capability in supported_capabilities: 235 supported_conditional_capabilities.add(capability) 236 237 logger.debug(f"Found supported capabilities {supported_conditional_capabilities}.") 238 return supported_conditional_capabilities 239 240 @classmethod 241 def _get_decorated_capabilities_map( 242 cls, 243 ) -> dict[TestPmdShellDecorator | None, set["DecoratedNicCapability"]]: 244 capabilities_map: dict[TestPmdShellDecorator | None, set["DecoratedNicCapability"]] = {} 245 for capability in cls.capabilities_to_check: 246 if capability.capability_decorator not in capabilities_map: 247 capabilities_map[capability.capability_decorator] = set() 248 capabilities_map[capability.capability_decorator].add(capability) 249 250 return capabilities_map 251 252 @classmethod 253 def _reduce_capabilities( 254 cls, 255 capabilities: set["DecoratedNicCapability"], 256 supported_capabilities: MutableSet, 257 unsupported_capabilities: MutableSet, 258 ) -> TestPmdShellMethod: 259 def reduced_fn(testpmd_shell: TestPmdShell) -> None: 260 for capability in capabilities: 261 if capability not in supported_capabilities | unsupported_capabilities: 262 capability.capability_fn( 263 testpmd_shell, supported_capabilities, unsupported_capabilities 264 ) 265 266 return reduced_fn 267 268 def __hash__(self) -> int: 269 """Instances are identified by :attr:`nic_capability` and :attr:`capability_decorator`.""" 270 return hash(self.nic_capability) 271 272 def __repr__(self) -> str: 273 """Easy to read string of :attr:`nic_capability` and :attr:`capability_decorator`.""" 274 return f"{self.nic_capability}" 275 276 277@dataclass 278class TopologyCapability(Capability): 279 """A wrapper around :class:`~.topology.TopologyType`. 280 281 Each test case must be assigned a topology. It could be done explicitly; 282 the implicit default is :attr:`~.topology.TopologyType.default`, which this class defines 283 as equal to :attr:`~.topology.TopologyType.two_links`. 284 285 Test case topology may be set by setting the topology for the whole suite. 286 The priority in which topology is set is as follows: 287 288 #. The topology set using the :func:`requires` decorator with a test case, 289 #. The topology set using the :func:`requires` decorator with a test suite, 290 #. The default topology if the decorator is not used. 291 292 The default topology of test suite (i.e. when not using the decorator 293 or not setting the topology with the decorator) does not affect the topology of test cases. 294 295 New instances should be created with the :meth:`create_unique` class method to ensure 296 there are no duplicate instances. 297 298 Attributes: 299 topology_type: The topology type that defines each instance. 300 """ 301 302 topology_type: TopologyType 303 304 _unique_capabilities: ClassVar[dict[str, Self]] = {} 305 306 def _preprocess_required(self, test_case_or_suite: type["TestProtocol"]) -> None: 307 test_case_or_suite.required_capabilities.discard(test_case_or_suite.topology_type) 308 test_case_or_suite.topology_type = self 309 310 @classmethod 311 def get_unique(cls, topology_type: TopologyType) -> "TopologyCapability": 312 """Get the capability uniquely identified by `topology_type`. 313 314 This is a factory method that implements a quasi-enum pattern. 315 The instances of this class are stored in an internal class variable, 316 `_unique_capabilities`. 317 318 If an instance identified by `topology_type` doesn't exist, 319 it is created and added to `_unique_capabilities`. 320 If it exists, it is returned so that a new identical instance is not created. 321 322 Args: 323 topology_type: The topology type. 324 325 Returns: 326 The capability uniquely identified by `topology_type`. 327 """ 328 if topology_type.name not in cls._unique_capabilities: 329 cls._unique_capabilities[topology_type.name] = cls(topology_type) 330 return cls._unique_capabilities[topology_type.name] 331 332 @classmethod 333 def get_supported_capabilities( 334 cls, sut_node: SutNode, topology: "Topology" 335 ) -> set["TopologyCapability"]: 336 """Overrides :meth:`~Capability.get_supported_capabilities`.""" 337 supported_capabilities = set() 338 topology_capability = cls.get_unique(topology.type) 339 for topology_type in TopologyType: 340 candidate_topology_type = cls.get_unique(topology_type) 341 if candidate_topology_type <= topology_capability: 342 supported_capabilities.add(candidate_topology_type) 343 return supported_capabilities 344 345 def set_required(self, test_case_or_suite: type["TestProtocol"]) -> None: 346 """The logic for setting the required topology of a test case or suite. 347 348 Decorators are applied on methods of a class first, then on the class. 349 This means we have to modify test case topologies when processing the test suite topologies. 350 At that point, the test case topologies have been set by the :func:`requires` decorator. 351 The test suite topology only affects the test case topologies 352 if not :attr:`~.topology.TopologyType.default`. 353 """ 354 if inspect.isclass(test_case_or_suite): 355 if self.topology_type is not TopologyType.default: 356 self.add_to_required(test_case_or_suite) 357 func_test_cases, perf_test_cases = test_case_or_suite.get_test_cases() 358 for test_case in func_test_cases | perf_test_cases: 359 if test_case.topology_type.topology_type is TopologyType.default: 360 # test case topology has not been set, use the one set by the test suite 361 self.add_to_required(test_case) 362 elif test_case.topology_type > test_case_or_suite.topology_type: 363 raise ConfigurationError( 364 "The required topology type of a test case " 365 f"({test_case.__name__}|{test_case.topology_type}) " 366 "cannot be more complex than that of a suite " 367 f"({test_case_or_suite.__name__}|{test_case_or_suite.topology_type})." 368 ) 369 else: 370 self.add_to_required(test_case_or_suite) 371 372 def __eq__(self, other) -> bool: 373 """Compare the :attr:`~TopologyCapability.topology_type`s. 374 375 Args: 376 other: The object to compare with. 377 378 Returns: 379 :data:`True` if the topology types are the same. 380 """ 381 return self.topology_type == other.topology_type 382 383 def __lt__(self, other) -> bool: 384 """Compare the :attr:`~TopologyCapability.topology_type`s. 385 386 Args: 387 other: The object to compare with. 388 389 Returns: 390 :data:`True` if the instance's topology type is less complex than the compared object's. 391 """ 392 return self.topology_type < other.topology_type 393 394 def __gt__(self, other) -> bool: 395 """Compare the :attr:`~TopologyCapability.topology_type`s. 396 397 Args: 398 other: The object to compare with. 399 400 Returns: 401 :data:`True` if the instance's topology type is more complex than the compared object's. 402 """ 403 return other < self 404 405 def __le__(self, other) -> bool: 406 """Compare the :attr:`~TopologyCapability.topology_type`s. 407 408 Args: 409 other: The object to compare with. 410 411 Returns: 412 :data:`True` if the instance's topology type is less complex or equal than 413 the compared object's. 414 """ 415 return not self > other 416 417 def __hash__(self): 418 """Each instance is identified by :attr:`topology_type`.""" 419 return self.topology_type.__hash__() 420 421 def __str__(self): 422 """Easy to read string of class and name of :attr:`topology_type`. 423 424 Converts :attr:`TopologyType.default` to the actual value. 425 """ 426 name = self.topology_type.name 427 if self.topology_type is TopologyType.default: 428 name = TopologyType.get_from_value(self.topology_type.value).name 429 return f"{type(self.topology_type).__name__}.{name}" 430 431 def __repr__(self): 432 """Easy to read string of class and name of :attr:`topology_type`.""" 433 return self.__str__() 434 435 436class TestProtocol(Protocol): 437 """Common test suite and test case attributes.""" 438 439 #: Whether to skip the test case or suite. 440 skip: ClassVar[bool] = False 441 #: The reason for skipping the test case or suite. 442 skip_reason: ClassVar[str] = "" 443 #: The topology type of the test case or suite. 444 topology_type: ClassVar[TopologyCapability] = TopologyCapability(TopologyType.default) 445 #: The capabilities the test case or suite requires in order to be executed. 446 required_capabilities: ClassVar[set[Capability]] = set() 447 448 @classmethod 449 def get_test_cases(cls, test_case_sublist: Sequence[str] | None = None) -> tuple[set, set]: 450 """Get test cases. Should be implemented by subclasses containing test cases. 451 452 Raises: 453 NotImplementedError: The subclass does not implement the method. 454 """ 455 raise NotImplementedError() 456 457 458def requires( 459 *nic_capabilities: NicCapability, 460 topology_type: TopologyType = TopologyType.default, 461) -> Callable[[type[TestProtocol]], type[TestProtocol]]: 462 """A decorator that adds the required capabilities to a test case or test suite. 463 464 Args: 465 nic_capabilities: The NIC capabilities that are required by the test case or test suite. 466 topology_type: The topology type the test suite or case requires. 467 468 Returns: 469 The decorated test case or test suite. 470 """ 471 472 def add_required_capability(test_case_or_suite: type[TestProtocol]) -> type[TestProtocol]: 473 for nic_capability in nic_capabilities: 474 decorated_nic_capability = DecoratedNicCapability.get_unique(nic_capability) 475 decorated_nic_capability.add_to_required(test_case_or_suite) 476 477 topology_capability = TopologyCapability.get_unique(topology_type) 478 topology_capability.set_required(test_case_or_suite) 479 480 return test_case_or_suite 481 482 return add_required_capability 483 484 485def get_supported_capabilities( 486 sut_node: SutNode, 487 topology_config: Topology, 488 capabilities_to_check: set[Capability], 489) -> set[Capability]: 490 """Probe the environment for `capabilities_to_check` and return the supported ones. 491 492 Args: 493 sut_node: The SUT node to check for capabilities. 494 topology_config: The topology config to check for capabilities. 495 capabilities_to_check: The capabilities to check. 496 497 Returns: 498 The capabilities supported by the environment. 499 """ 500 callbacks = set() 501 for capability_to_check in capabilities_to_check: 502 callbacks.add(capability_to_check.register_to_check()) 503 supported_capabilities = set() 504 for callback in callbacks: 505 supported_capabilities.update(callback(sut_node, topology_config)) 506 507 return supported_capabilities 508