File: capability.py

package info (click to toggle)
dpdk 25.11-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 127,892 kB
  • sloc: ansic: 2,358,479; python: 16,426; sh: 4,474; makefile: 1,713; awk: 70
file content (719 lines) | stat: -rw-r--r-- 31,697 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
# SPDX-License-Identifier: BSD-3-Clause
# Copyright(c) 2024 PANTHEON.tech s.r.o.
# Copyright(c) 2025 Arm Limited

"""Testbed capabilities.

This module provides a protocol that defines the common attributes of test cases and suites
and support for test environment capabilities.

Many test cases are testing features not available on all hardware.
On the other hand, some test cases or suites may not need the most complex topology available.

The module allows developers to mark test cases or suites a requiring certain hardware capabilities
or a particular topology with the :func:`requires` decorator.

There are differences between hardware and topology capabilities:

    * Hardware capabilities are assumed to not be required when not specified.
    * However, some topology is always available, so each test case or suite is assigned
      a default topology if no topology is specified in the decorator.

The module also allows developers to mark test cases or suites as requiring certain
hardware capabilities with the :func:`requires` decorator.

Examples:
    .. code:: python

        from framework.test_suite import TestSuite, func_test
        from framework.testbed_model.capability import LinkTopology, requires
        # The whole test suite (each test case within) doesn't require any links.
        @requires_link_topology(LinkTopology.NO_LINK)
        @func_test
        class TestHelloWorld(TestSuite):
            def hello_world_single_core(self):
            ...

    .. code:: python

        from framework.test_suite import TestSuite, func_test
        from framework.testbed_model.capability import NicCapability, requires
        class TestPmdBufferScatter(TestSuite):
            # only the test case requires the SCATTERED_RX_ENABLED capability
            # other test cases may not require it
            @requires_nic_capability(NicCapability.SCATTERED_RX_ENABLED)
            @func_test
            def test_scatter_mbuf_2048(self):
"""

import inspect
from abc import ABC, abstractmethod
from collections.abc import MutableSet
from dataclasses import dataclass
from typing import (
    TYPE_CHECKING,
    Any,
    Callable,
    ClassVar,
    Concatenate,
    ParamSpec,
    Protocol,
    TypeAlias,
)

from typing_extensions import Self

from api.capabilities import LinkTopology, NicCapability
from framework.exception import ConfigurationError, InternalError, SkippedTestException
from framework.logger import get_dts_logger
from framework.testbed_model.node import Node
from framework.testbed_model.port import DriverKind
from framework.testbed_model.topology import Topology

if TYPE_CHECKING:
    from api.testpmd import TestPmd
    from framework.test_suite import TestCase

P = ParamSpec("P")
TestPmdMethod = Callable[Concatenate["TestPmd", P], Any]
TestPmdCapabilityMethod: TypeAlias = Callable[
    ["TestPmd", MutableSet["NicCapability"], MutableSet["NicCapability"]], None
]
TestPmdDecorator: TypeAlias = Callable[[TestPmdMethod], TestPmdMethod]
TestPmdNicCapability = tuple[TestPmdCapabilityMethod, TestPmdDecorator | None]


class Capability(ABC):
    """The base class for various capabilities.

    The same capability should always be represented by the same object,
    meaning the same capability required by different test cases or suites
    should point to the same object.

    Example:
        ``test_case1`` and ``test_case2`` each require ``capability1``
        and in both instances, ``capability1`` should point to the same capability object.

    It is up to the subclasses how they implement this.

    The instances are used in sets so they must be hashable.
    """

    #: A set storing the capabilities whose support should be checked.
    capabilities_to_check: ClassVar[set[Self]] = set()

    def register_to_check(self) -> Callable[[Node, "Topology"], set[Self]]:
        """Register the capability to be checked for support.

        Returns:
            The callback function that checks the support of capabilities of the particular subclass
            which should be called after all capabilities have been registered.
        """
        if not type(self).capabilities_to_check:
            type(self).capabilities_to_check = set()
        type(self).capabilities_to_check.add(self)
        return type(self)._get_and_reset

    def add_to_required(self, test_case_or_suite: type["TestProtocol"]) -> None:
        """Add the capability instance to the required test case or suite's capabilities.

        Args:
            test_case_or_suite: The test case or suite among whose required capabilities
                to add this instance.
        """
        if not test_case_or_suite.required_capabilities:
            test_case_or_suite.required_capabilities = set()
        self._preprocess_required(test_case_or_suite)
        test_case_or_suite.required_capabilities.add(self)

    def _preprocess_required(self, test_case_or_suite: type["TestProtocol"]) -> None:
        """An optional method that modifies the required capabilities."""

    @classmethod
    def _get_and_reset(cls, node: Node, topology: "Topology") -> set[Self]:
        """The callback method to be called after all capabilities have been registered.

        Not only does this method check the support of capabilities,
        but it also reset the internal set of registered capabilities
        so that the "register, then get support" workflow works in subsequent test runs.
        """
        supported_capabilities = cls.get_supported_capabilities(node, topology)
        cls.capabilities_to_check = set()
        return supported_capabilities

    @classmethod
    @abstractmethod
    def get_supported_capabilities(cls, node: Node, topology: "Topology") -> set[Self]:
        """Get the support status of each registered capability.

        Each subclass must implement this method and return the subset of supported capabilities
        of :attr:`capabilities_to_check`.

        Args:
            node: The node to check capabilities against.
            topology: The topology of the current test run.

        Returns:
            The supported capabilities.
        """

    @abstractmethod
    def __hash__(self) -> int:
        """The subclasses must be hashable so that they can be stored in sets."""

    def is_comparable_with(self, other: Any) -> bool:
        """Check if the other object is of the same type for comparison.

        Args:
            other: The object to compare with.

        Returns:
            True if the other object is of the same type, False otherwise.
        """
        return isinstance(other, type(self))


@dataclass
class DecoratedNicCapability(Capability):
    """A wrapper around :class:`~api.testpmd.NicCapability`.

    New instances should be created with the :meth:`create_unique` class method to ensure
    there are no duplicate instances.

    Attributes:
        nic_capability: The NIC capability that defines each instance.
        capability_fn: The capability retrieval function of `nic_capability`.
        capability_decorator: The decorator function of `nic_capability`.
            This function will wrap `capability_fn`.
    """

    nic_capability: NicCapability
    capability_fn: TestPmdCapabilityMethod
    capability_decorator: TestPmdDecorator | None
    _unique_capabilities: ClassVar[dict[NicCapability, Self]] = {}

    @classmethod
    def _get_nic_capability_check(cls) -> list[TestPmdNicCapability]:
        """A mapping between capability names and the associated :class:`TestPmd` methods.

        The :class:`TestPmd` capability checking method executes the command that checks
        whether the capability is supported.
        A decorator may optionally be added to the method that will add and remove configuration
        that's necessary to retrieve the capability support status.
        The Enum members may be assigned the method itself or a tuple of
        (capability_checking_method, decorator_function).

        The signature of each :class:`TestPmd` capability checking method must be::

            fn(self, supported_capabilities: MutableSet, unsupported_capabilities: MutableSet)

        The capability checking method must get whether a capability is supported or not
        from a testpmd command. If multiple capabilities can be obtained from a testpmd command,
        each should be obtained in the method. These capabilities should then
        be added to `supported_capabilities` or `unsupported_capabilities` based on their support.

        The two dictionaries are shared across all capability discovery function calls in a given
        test run so that we don't call the same function multiple times. For example, when we find
        :attr:`SCATTERED_RX_ENABLED` in :meth:`TestPmd.get_capabilities_rxq_info`,
        we don't go looking for it again if a different test case also needs it.
        """
        from api.testpmd import TestPmd, _add_remove_mtu

        # In order to guard against creating new NicCapability enum members without adding
        # the evaluation code below we use a case statement that will trigger the linter
        # enforcing that all cases are handled.
        def mapping(cap: NicCapability) -> TestPmdNicCapability:
            match cap:
                case NicCapability.SCATTERED_RX_ENABLED:
                    return (TestPmd.get_capabilities_rxq_info, _add_remove_mtu(9000))
                case (
                    NicCapability.PORT_RX_OFFLOAD_VLAN_STRIP
                    | NicCapability.PORT_RX_OFFLOAD_IPV4_CKSUM
                    | NicCapability.PORT_RX_OFFLOAD_UDP_CKSUM
                    | NicCapability.PORT_RX_OFFLOAD_TCP_CKSUM
                    | NicCapability.PORT_RX_OFFLOAD_TCP_LRO
                    | NicCapability.PORT_RX_OFFLOAD_QINQ_STRIP
                    | NicCapability.PORT_RX_OFFLOAD_OUTER_IPV4_CKSUM
                    | NicCapability.PORT_RX_OFFLOAD_MACSEC_STRIP
                    | NicCapability.PORT_RX_OFFLOAD_VLAN_FILTER
                    | NicCapability.PORT_RX_OFFLOAD_VLAN_EXTEND
                    | NicCapability.PORT_RX_OFFLOAD_SCATTER
                    | NicCapability.PORT_RX_OFFLOAD_TIMESTAMP
                    | NicCapability.PORT_RX_OFFLOAD_SECURITY
                    | NicCapability.PORT_RX_OFFLOAD_KEEP_CRC
                    | NicCapability.PORT_RX_OFFLOAD_SCTP_CKSUM
                    | NicCapability.PORT_RX_OFFLOAD_OUTER_UDP_CKSUM
                    | NicCapability.PORT_RX_OFFLOAD_RSS_HASH
                    | NicCapability.PORT_RX_OFFLOAD_BUFFER_SPLIT
                    | NicCapability.PORT_RX_OFFLOAD_CHECKSUM
                    | NicCapability.PORT_RX_OFFLOAD_VLAN
                    | NicCapability.QUEUE_RX_OFFLOAD_VLAN_STRIP
                    | NicCapability.QUEUE_RX_OFFLOAD_IPV4_CKSUM
                    | NicCapability.QUEUE_RX_OFFLOAD_UDP_CKSUM
                    | NicCapability.QUEUE_RX_OFFLOAD_TCP_CKSUM
                    | NicCapability.QUEUE_RX_OFFLOAD_TCP_LRO
                    | NicCapability.QUEUE_RX_OFFLOAD_QINQ_STRIP
                    | NicCapability.QUEUE_RX_OFFLOAD_OUTER_IPV4_CKSUM
                    | NicCapability.QUEUE_RX_OFFLOAD_MACSEC_STRIP
                    | NicCapability.QUEUE_RX_OFFLOAD_VLAN_FILTER
                    | NicCapability.QUEUE_RX_OFFLOAD_VLAN_EXTEND
                    | NicCapability.QUEUE_RX_OFFLOAD_SCATTER
                    | NicCapability.QUEUE_RX_OFFLOAD_TIMESTAMP
                    | NicCapability.QUEUE_RX_OFFLOAD_SECURITY
                    | NicCapability.QUEUE_RX_OFFLOAD_KEEP_CRC
                    | NicCapability.QUEUE_RX_OFFLOAD_SCTP_CKSUM
                    | NicCapability.QUEUE_RX_OFFLOAD_OUTER_UDP_CKSUM
                    | NicCapability.QUEUE_RX_OFFLOAD_RSS_HASH
                    | NicCapability.QUEUE_RX_OFFLOAD_BUFFER_SPLIT
                    | NicCapability.QUEUE_RX_OFFLOAD_CHECKSUM
                    | NicCapability.QUEUE_RX_OFFLOAD_VLAN
                ):
                    return (TestPmd.get_offload_capabilities_func("rx"), None)
                case (
                    NicCapability.PORT_TX_OFFLOAD_VLAN_INSERT
                    | NicCapability.PORT_TX_OFFLOAD_IPV4_CKSUM
                    | NicCapability.PORT_TX_OFFLOAD_UDP_CKSUM
                    | NicCapability.PORT_TX_OFFLOAD_TCP_CKSUM
                    | NicCapability.PORT_TX_OFFLOAD_SCTP_CKSUM
                    | NicCapability.PORT_TX_OFFLOAD_TCP_TSO
                    | NicCapability.PORT_TX_OFFLOAD_UDP_TSO
                    | NicCapability.PORT_TX_OFFLOAD_OUTER_IPV4_CKSUM
                    | NicCapability.PORT_TX_OFFLOAD_QINQ_INSERT
                    | NicCapability.PORT_TX_OFFLOAD_VXLAN_TNL_TSO
                    | NicCapability.PORT_TX_OFFLOAD_GRE_TNL_TSO
                    | NicCapability.PORT_TX_OFFLOAD_IPIP_TNL_TSO
                    | NicCapability.PORT_TX_OFFLOAD_GENEVE_TNL_TSO
                    | NicCapability.PORT_TX_OFFLOAD_MACSEC_INSERT
                    | NicCapability.PORT_TX_OFFLOAD_MT_LOCKFREE
                    | NicCapability.PORT_TX_OFFLOAD_MULTI_SEGS
                    | NicCapability.PORT_TX_OFFLOAD_MBUF_FAST_FREE
                    | NicCapability.PORT_TX_OFFLOAD_SECURITY
                    | NicCapability.PORT_TX_OFFLOAD_UDP_TNL_TSO
                    | NicCapability.PORT_TX_OFFLOAD_IP_TNL_TSO
                    | NicCapability.PORT_TX_OFFLOAD_OUTER_UDP_CKSUM
                    | NicCapability.PORT_TX_OFFLOAD_SEND_ON_TIMESTAMP
                    | NicCapability.QUEUE_TX_OFFLOAD_VLAN_INSERT
                    | NicCapability.QUEUE_TX_OFFLOAD_IPV4_CKSUM
                    | NicCapability.QUEUE_TX_OFFLOAD_UDP_CKSUM
                    | NicCapability.QUEUE_TX_OFFLOAD_TCP_CKSUM
                    | NicCapability.QUEUE_TX_OFFLOAD_SCTP_CKSUM
                    | NicCapability.QUEUE_TX_OFFLOAD_TCP_TSO
                    | NicCapability.QUEUE_TX_OFFLOAD_UDP_TSO
                    | NicCapability.QUEUE_TX_OFFLOAD_OUTER_IPV4_CKSUM
                    | NicCapability.QUEUE_TX_OFFLOAD_QINQ_INSERT
                    | NicCapability.QUEUE_TX_OFFLOAD_VXLAN_TNL_TSO
                    | NicCapability.QUEUE_TX_OFFLOAD_GRE_TNL_TSO
                    | NicCapability.QUEUE_TX_OFFLOAD_IPIP_TNL_TSO
                    | NicCapability.QUEUE_TX_OFFLOAD_GENEVE_TNL_TSO
                    | NicCapability.QUEUE_TX_OFFLOAD_MACSEC_INSERT
                    | NicCapability.QUEUE_TX_OFFLOAD_MT_LOCKFREE
                    | NicCapability.QUEUE_TX_OFFLOAD_MULTI_SEGS
                    | NicCapability.QUEUE_TX_OFFLOAD_MBUF_FAST_FREE
                    | NicCapability.QUEUE_TX_OFFLOAD_SECURITY
                    | NicCapability.QUEUE_TX_OFFLOAD_UDP_TNL_TSO
                    | NicCapability.QUEUE_TX_OFFLOAD_IP_TNL_TSO
                    | NicCapability.QUEUE_TX_OFFLOAD_OUTER_UDP_CKSUM
                    | NicCapability.QUEUE_TX_OFFLOAD_SEND_ON_TIMESTAMP
                ):
                    return (TestPmd.get_offload_capabilities_func("tx"), None)
                case (
                    NicCapability.RUNTIME_RX_QUEUE_SETUP
                    | NicCapability.RUNTIME_TX_QUEUE_SETUP
                    | NicCapability.RXQ_SHARE
                    | NicCapability.FLOW_RULE_KEEP
                    | NicCapability.FLOW_SHARED_OBJECT_KEEP
                ):
                    return (TestPmd.get_capabilities_show_port_info, None)
                case NicCapability.MCAST_FILTERING:
                    return (TestPmd.get_capabilities_mcast_filtering, None)
                case NicCapability.FLOW_CTRL:
                    return (TestPmd.get_capabilities_flow_ctrl, None)
                case NicCapability.PHYSICAL_FUNCTION:
                    return (TestPmd.get_capabilities_physical_function, None)

        return [mapping(cap) for cap in NicCapability]

    @classmethod
    def get_unique(cls, nic_capability: NicCapability) -> Self:
        """Get the capability uniquely identified by `nic_capability`.

        This is a factory method that implements a quasi-enum pattern.
        The instances of this class are stored in an internal class variable,
        `_unique_capabilities`.

        If an instance identified by `nic_capability` doesn't exist,
        it is created and added to `_unique_capabilities`.
        If it exists, it is returned so that a new identical instance is not created.

        Args:
            nic_capability: The NIC capability.

        Returns:
            The capability uniquely identified by `nic_capability`.
        """
        capability_fn, decorator_fn = cls._get_nic_capability_check()[nic_capability.value]

        if nic_capability not in cls._unique_capabilities:
            cls._unique_capabilities[nic_capability] = cls(
                nic_capability, capability_fn, decorator_fn
            )
        return cls._unique_capabilities[nic_capability]

    @classmethod
    def get_supported_capabilities(
        cls, node: Node, topology: "Topology"
    ) -> set["DecoratedNicCapability"]:
        """Overrides :meth:`~Capability.get_supported_capabilities`.

        The capabilities are first sorted by decorators, then reduced into a single function which
        is then passed to the decorator. This way we execute each decorator only once.
        Each capability is first checked whether it's supported/unsupported
        before executing its `capability_fn` so that each capability is retrieved only once.
        """
        from api.testpmd import TestPmd

        supported_conditional_capabilities: set["DecoratedNicCapability"] = set()
        logger = get_dts_logger(f"{node.name}.{cls.__name__}")
        if topology.type is topology.type.NO_LINK:
            logger.debug(
                "No links available in the current topology, not getting NIC capabilities."
            )
            return supported_conditional_capabilities
        logger.debug(
            f"Checking which NIC capabilities from {cls.capabilities_to_check} are supported."
        )
        if cls.capabilities_to_check:
            capabilities_to_check_map = cls._get_decorated_capabilities_map()
            with TestPmd() as testpmd:
                for (
                    conditional_capability_fn,
                    capabilities,
                ) in capabilities_to_check_map.items():
                    supported_capabilities: set[NicCapability] = set()
                    unsupported_capabilities: set[NicCapability] = set()
                    capability_fn = cls._reduce_capabilities(
                        capabilities, supported_capabilities, unsupported_capabilities
                    )
                    if conditional_capability_fn:
                        capability_fn = conditional_capability_fn(capability_fn)
                    capability_fn(testpmd)
                    for capability in capabilities:
                        if capability.nic_capability in supported_capabilities:
                            supported_conditional_capabilities.add(capability)

        logger.debug(f"Found supported capabilities {supported_conditional_capabilities}.")
        return supported_conditional_capabilities

    @classmethod
    def _get_decorated_capabilities_map(
        cls,
    ) -> dict[TestPmdDecorator | None, set["DecoratedNicCapability"]]:
        capabilities_map: dict[TestPmdDecorator | None, set["DecoratedNicCapability"]] = {}
        for capability in cls.capabilities_to_check:
            if capability.capability_decorator not in capabilities_map:
                capabilities_map[capability.capability_decorator] = set()
            capabilities_map[capability.capability_decorator].add(capability)

        return capabilities_map

    @classmethod
    def _reduce_capabilities(
        cls,
        capabilities: set["DecoratedNicCapability"],
        supported_capabilities: MutableSet,
        unsupported_capabilities: MutableSet,
    ) -> TestPmdMethod:
        def reduced_fn(testpmd: "TestPmd") -> None:
            for capability in capabilities:
                if capability not in supported_capabilities | unsupported_capabilities:
                    capability.capability_fn(
                        testpmd, supported_capabilities, unsupported_capabilities
                    )

        return reduced_fn

    def __hash__(self) -> int:
        """Instances are identified by :attr:`nic_capability` and :attr:`capability_decorator`."""
        return hash(self.nic_capability)

    def __repr__(self) -> str:
        """Easy to read string of :attr:`nic_capability` and :attr:`capability_decorator`."""
        return f"{self.nic_capability}"


@dataclass
class TopologyCapability(Capability):
    """A wrapper around :class:`~.topology.LinkTopology`.

    Each test case must be assigned a topology. It could be done explicitly;
    the implicit default is given by :meth:`~.topology.LinkTopology.default`, which this class
    returns :attr:`~.topology.LinkTopology.TWO_LINKS`.

    Test case topology may be set by setting the topology for the whole suite.
    The priority in which topology is set is as follows:

        #. The topology set using the :func:`requires` decorator with a test case,
        #. The topology set using the :func:`requires` decorator with a test suite,
        #. The default topology if the decorator is not used.

    The default topology of test suite (i.e. when not using the decorator
    or not setting the topology with the decorator) does not affect the topology of test cases.

    New instances should be created with the :meth:`create_unique` class method to ensure
    there are no duplicate instances.

    Attributes:
        topology_type: The topology type that defines each instance.
    """

    topology_type: LinkTopology

    _unique_capabilities: ClassVar[dict[str, Self]] = {}

    def _preprocess_required(self, test_case_or_suite: type["TestProtocol"]) -> None:
        test_case_or_suite.required_capabilities.discard(test_case_or_suite.topology_type)
        test_case_or_suite.topology_type = self

    @classmethod
    def get_unique(cls, topology_type: LinkTopology) -> Self:
        """Get the capability uniquely identified by `topology_type`.

        This is a factory method that implements a quasi-enum pattern.
        The instances of this class are stored in an internal class variable,
        `_unique_capabilities`.

        If an instance identified by `topology_type` doesn't exist,
        it is created and added to `_unique_capabilities`.
        If it exists, it is returned so that a new identical instance is not created.

        Args:
            topology_type: The topology type.

        Returns:
            The capability uniquely identified by `topology_type`.
        """
        if topology_type.name not in cls._unique_capabilities:
            cls._unique_capabilities[topology_type.name] = cls(topology_type)
        return cls._unique_capabilities[topology_type.name]

    @classmethod
    def get_supported_capabilities(
        cls, node: Node, topology: "Topology"
    ) -> set["TopologyCapability"]:
        """Overrides :meth:`~Capability.get_supported_capabilities`."""
        supported_capabilities = set()
        topology_capability = cls.get_unique(topology.type)
        for topology_type in LinkTopology:
            candidate_topology_type = cls.get_unique(topology_type)
            if candidate_topology_type <= topology_capability:
                supported_capabilities.add(candidate_topology_type)
        return supported_capabilities

    def set_required(self, test_case_or_suite: type["TestProtocol"]) -> None:
        """The logic for setting the required topology of a test case or suite.

        Decorators are applied on methods of a class first, then on the class.
        This means we have to modify test case topologies when processing the test suite topologies.
        At that point, the test case topologies have been set by the :func:`requires` decorator.
        The test suite topology only affects the test case topologies
        if not :attr:`~.topology.LinkTopology.default`.

        Raises:
            ConfigurationError: If the topology type requested by the test case is more complex than
                the test suite's.
        """
        if inspect.isclass(test_case_or_suite):
            if self.topology_type is not LinkTopology.default():
                self.add_to_required(test_case_or_suite)
                for test_case in test_case_or_suite.get_test_cases():
                    if test_case.topology_type.topology_type is LinkTopology.default():
                        # test case topology has not been set, use the one set by the test suite
                        self.add_to_required(test_case)
                    elif test_case.topology_type > test_case_or_suite.topology_type:
                        raise ConfigurationError(
                            "The required topology type of a test case "
                            f"({test_case.__name__}|{test_case.topology_type}) "
                            "cannot be more complex than that of a suite "
                            f"({test_case_or_suite.__name__}|{test_case_or_suite.topology_type})."
                        )
        else:
            self.add_to_required(test_case_or_suite)

    def __eq__(self, other: Any) -> bool:
        """Compare the :attr:`~TopologyCapability.topology_type`s.

        Args:
            other: The object to compare with.

        Returns:
            :data:`True` if the topology types are the same.
        """
        if not self.is_comparable_with(other):
            return False
        return self.topology_type == other.topology_type

    def __lt__(self, other: Any) -> bool:
        """Compare the :attr:`~TopologyCapability.topology_type`s.

        Args:
            other: The object to compare with.

        Returns:
            :data:`True` if the instance's topology type is less complex than the compared object's.
        """
        if not self.is_comparable_with(other):
            return False
        return self.topology_type < other.topology_type

    def __gt__(self, other: Any) -> bool:
        """Compare the :attr:`~TopologyCapability.topology_type`s.

        Args:
            other: The object to compare with.

        Returns:
            :data:`True` if the instance's topology type is more complex than the compared object's.
        """
        return other < self

    def __le__(self, other: Any) -> bool:
        """Compare the :attr:`~TopologyCapability.topology_type`s.

        Args:
            other: The object to compare with.

        Returns:
            :data:`True` if the instance's topology type is less complex or equal than
            the compared object's.
        """
        return not self > other

    def __hash__(self):
        """Each instance is identified by :attr:`topology_type`."""
        return self.topology_type.__hash__()

    def __str__(self):
        """Easy to read string of class and name of :attr:`topology_type`."""
        return f"{type(self.topology_type).__name__}.{self.topology_type.name}"

    def __repr__(self):
        """Easy to read string of class and name of :attr:`topology_type`."""
        return self.__str__()


class TestProtocol(Protocol):
    """Common test suite and test case attributes."""

    #: Whether to skip the test case or suite.
    skip: ClassVar[bool] = False
    #: The reason for skipping the test case or suite.
    skip_reason: ClassVar[str] = ""
    #: The topology type of the test case or suite.
    topology_type: ClassVar[TopologyCapability] = TopologyCapability(LinkTopology.default())
    #: The capabilities the test case or suite requires in order to be executed.
    required_capabilities: ClassVar[set[Capability]] = set()
    #: The SUT ports topology configuration of the test case or suite.
    sut_ports_drivers: ClassVar[DriverKind | tuple[DriverKind, ...] | None] = None

    @classmethod
    def get_test_cases(cls) -> list[type["TestCase"]]:
        """Get test cases. Should be implemented by subclasses containing test cases.

        Raises:
            NotImplementedError: The subclass does not implement the method.
        """
        raise NotImplementedError()


def configure_ports(
    *drivers: DriverKind, all_for: DriverKind | None = None
) -> Callable[[type[TestProtocol]], type[TestProtocol]]:
    """Decorator for test suite and test cases to configure ports drivers.

    Configure all the SUT ports for the specified driver kind with `all_for`. Otherwise, specify
    the port's respective driver kind in the positional argument. The amount of ports specified must
    adhere to the requested topology.

    Raises:
        InternalError: If both positional arguments and `all_for` are set.
    """
    if len(drivers) and all_for is not None:
        msg = "Cannot set both positional arguments and `all_for` to configure ports drivers."
        raise InternalError(msg)

    def _decorator(func: type[TestProtocol]) -> type[TestProtocol]:
        func.sut_ports_drivers = all_for or drivers
        return func

    return _decorator


def requires(
    *nic_capabilities: NicCapability,
    topology_type: LinkTopology = LinkTopology.default(),
) -> Callable[[type[TestProtocol]], type[TestProtocol]]:
    """A decorator that adds the required capabilities to a test case or test suite.

    Args:
        nic_capabilities: The NIC capabilities that are required by the test case or test suite.
        topology_type: The topology type the test suite or case requires.

    Returns:
        The decorated test case or test suite.
    """

    def add_required_capability(
        test_case_or_suite: type[TestProtocol],
    ) -> type[TestProtocol]:
        for nic_capability in nic_capabilities:
            decorated_nic_capability = DecoratedNicCapability.get_unique(nic_capability)
            decorated_nic_capability.add_to_required(test_case_or_suite)

        topology_capability = TopologyCapability.get_unique(topology_type)
        topology_capability.set_required(test_case_or_suite)

        return test_case_or_suite

    return add_required_capability


def get_supported_capabilities(
    node: Node,
    topology_config: Topology,
    capabilities_to_check: set[Capability],
) -> set[Capability]:
    """Probe the environment for `capabilities_to_check` and return the supported ones.

    Args:
        node: The node to check capabilities against.
        topology_config: The topology config to check for capabilities.
        capabilities_to_check: The capabilities to check.

    Returns:
        The capabilities supported by the environment.
    """
    callbacks = set()
    for capability_to_check in capabilities_to_check:
        callbacks.add(capability_to_check.register_to_check())
    supported_capabilities = set()
    for callback in callbacks:
        supported_capabilities.update(callback(node, topology_config))

    return supported_capabilities


def test_if_supported(test: type[TestProtocol], supported_caps: set[Capability]) -> None:
    """Test if the given test suite or test case is supported.

    Args:
        test: The test suite or case.
        supported_caps: The capabilities that need to be checked against the test.

    Raises:
        SkippedTestException: If the test hasn't met the requirements.
    """
    unsupported_caps = test.required_capabilities - supported_caps
    if unsupported_caps:
        capability_str = "capabilities" if len(unsupported_caps) > 1 else "capability"
        msg = f"Required {capability_str} '{unsupported_caps}' not found."
        raise SkippedTestException(msg)