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