I think this implementation is great, and I plan on testing it properly with the jumbo frames suite that I am developing before giving the final review. The only input that I could reasonably give is a couple rewordings on the docstrings which I'll highlight below.
On Thu, Apr 11, 2024 at 4:48 AM Juraj Linkeš <juraj.lin...@pantheon.tech> wrote: > > The devices under test may not support the capabilities required by > various test cases. Add support for: > 1. Marking test suites and test cases with required capabilities, > 2. Getting which required capabilities are supported by the device under > test, > 3. And then skipping test suites and test cases if their required > capabilities are not supported by the device. > > Signed-off-by: Juraj Linkeš <juraj.lin...@pantheon.tech> > --- > dts/framework/remote_session/__init__.py | 2 +- > dts/framework/remote_session/testpmd_shell.py | 44 ++++++++- > dts/framework/runner.py | 46 ++++++++-- > dts/framework/test_result.py | 90 +++++++++++++++---- > dts/framework/test_suite.py | 25 ++++++ > dts/framework/testbed_model/sut_node.py | 25 +++++- > dts/tests/TestSuite_hello_world.py | 4 +- > 7 files changed, 204 insertions(+), 32 deletions(-) > > diff --git a/dts/framework/remote_session/__init__.py > b/dts/framework/remote_session/__init__.py > index 1910c81c3c..f18a9f2259 100644 > --- a/dts/framework/remote_session/__init__.py > +++ b/dts/framework/remote_session/__init__.py > @@ -22,7 +22,7 @@ > from .python_shell import PythonShell > from .remote_session import CommandResult, RemoteSession > from .ssh_session import SSHSession > -from .testpmd_shell import TestPmdShell > +from .testpmd_shell import NicCapability, TestPmdShell > > > def create_remote_session( > diff --git a/dts/framework/remote_session/testpmd_shell.py > b/dts/framework/remote_session/testpmd_shell.py > index cb2ab6bd00..f6783af621 100644 > --- a/dts/framework/remote_session/testpmd_shell.py > +++ b/dts/framework/remote_session/testpmd_shell.py > @@ -16,7 +16,9 @@ > """ > > import time > -from enum import auto > +from collections.abc import MutableSet > +from enum import Enum, auto > +from functools import partial > from pathlib import PurePath > from typing import Callable, ClassVar > > @@ -229,3 +231,43 @@ def close(self) -> None: > """Overrides :meth:`~.interactive_shell.close`.""" > self.send_command("quit", "") > return super().close() > + > + def get_capas_rxq( > + self, supported_capabilities: MutableSet, unsupported_capabilities: > MutableSet > + ) -> None: > + """Get all rxq capabilities and divide them into supported and > unsupported. > + > + Args: > + supported_capabilities: A set where capabilities which are > supported will be stored. > + unsupported_capabilities: A set where capabilities which are > + not supported will be stored. Maybe change the arg descriptions to something like "A set where supported capabilities are stored" and "A set where unsupported capabilities are stored." > + """ > + self._logger.debug("Getting rxq capabilities.") > + command = "show rxq info 0 0" > + rxq_info = self.send_command(command) > + for line in rxq_info.split("\n"): > + bare_line = line.strip() > + if bare_line.startswith("RX scattered packets:"): > + if bare_line.endswith("on"): > + supported_capabilities.add(NicCapability.scattered_rx) > + else: > + unsupported_capabilities.add(NicCapability.scattered_rx) > + > + > +class NicCapability(Enum): > + """A mapping between capability names and the associated > :class:`TestPmdShell` methods. > + > + The :class:`TestPmdShell` method executes the command that checks > + whether the capability is supported. > + > + The signature of each :class:`TestPmdShell` method must be:: > + > + fn(self, supported_capabilities: MutableSet, > unsupported_capabilities: MutableSet) -> None > + > + The function must execute the testpmd command from which the capability > support can be obtained. "Which capability supported can be obtained." I think there was tense error here. > + If multiple capabilities can be obtained from the same testpmd command, > each should be obtained > + in one function. These capabilities should then be added to > `supported_capabilities` or > + `unsupported_capabilities` based on their support. > + """ > + > + scattered_rx = partial(TestPmdShell.get_capas_rxq) > diff --git a/dts/framework/runner.py b/dts/framework/runner.py > index db8e3ba96b..7407ea30b8 100644 > --- a/dts/framework/runner.py > +++ b/dts/framework/runner.py > @@ -501,6 +501,12 @@ def _run_test_suites( > The method assumes the build target we're testing has already been > built on the SUT node. > The current build target thus corresponds to the current DPDK build > present on the SUT node. > > + Before running any suites, the method determines whether they should > be skipped > + by inspecting any required capabilities the test suite needs and > comparing those > + to capabilities supported by the tested NIC. If all capabilities are > supported, > + the suite is run. If all test cases in a test suite would be > skipped, the whole test suite > + is skipped (the setup and teardown is not run). > + > If a blocking test suite (such as the smoke test suite) fails, the > rest of the test suites > in the current build target won't be executed. > > @@ -512,10 +518,30 @@ def _run_test_suites( > test_suites_with_cases: The test suites with test cases to run. > """ > end_build_target = False > + required_capabilities = set() > + supported_capabilities = set() > + for test_suite_with_cases in test_suites_with_cases: > + > required_capabilities.update(test_suite_with_cases.req_capabilities) > + self._logger.debug(f"Found required capabilities: > {required_capabilities}") > + if required_capabilities: > + supported_capabilities = > sut_node.get_supported_capabilities(required_capabilities) > for test_suite_with_cases in test_suites_with_cases: > test_suite_result = > build_target_result.add_test_suite(test_suite_with_cases) > + test_suite_with_cases.mark_skip(supported_capabilities) > try: > - self._run_test_suite(sut_node, tg_node, test_suite_result, > test_suite_with_cases) > + if test_suite_with_cases: > + self._run_test_suite( > + sut_node, > + tg_node, > + test_suite_result, > + test_suite_with_cases, > + ) > + else: > + self._logger.info( > + f"Test suite execution SKIPPED: " > + f"{test_suite_with_cases.test_suite_class.__name__}" > + ) > + test_suite_result.update_setup(Result.SKIP) > except BlockingTestSuiteError as e: > self._logger.exception( > f"An error occurred within > {test_suite_with_cases.test_suite_class.__name__}. " > @@ -614,14 +640,18 @@ def _execute_test_suite( > test_case_result = > test_suite_result.add_test_case(test_case_name) > all_attempts = SETTINGS.re_run + 1 > attempt_nr = 1 > - self._run_test_case(test_suite, test_case_method, > test_case_result) > - while not test_case_result and attempt_nr < all_attempts: > - attempt_nr += 1 > - self._logger.info( > - f"Re-running FAILED test case '{test_case_name}'. " > - f"Attempt number {attempt_nr} out of {all_attempts}." > - ) > + if TestSuiteWithCases.should_not_be_skipped(test_case_method): > self._run_test_case(test_suite, test_case_method, > test_case_result) > + while not test_case_result and attempt_nr < all_attempts: > + attempt_nr += 1 > + self._logger.info( > + f"Re-running FAILED test case '{test_case_name}'. " > + f"Attempt number {attempt_nr} out of {all_attempts}." > + ) > + self._run_test_case(test_suite, test_case_method, > test_case_result) > + else: > + self._logger.info(f"Test case execution SKIPPED: > {test_case_name}") > + test_case_result.update_setup(Result.SKIP) > > def _run_test_case( > self, > diff --git a/dts/framework/test_result.py b/dts/framework/test_result.py > index 28f84fd793..26c58a8606 100644 > --- a/dts/framework/test_result.py > +++ b/dts/framework/test_result.py > @@ -24,12 +24,14 @@ > """ > > import os.path > -from collections.abc import MutableSequence > -from dataclasses import dataclass > +from collections.abc import MutableSequence, MutableSet > +from dataclasses import dataclass, field > from enum import Enum, auto > from types import MethodType > from typing import Union > > +from framework.remote_session import NicCapability > + > from .config import ( > OS, > Architecture, > @@ -64,6 +66,14 @@ class is to hold a subset of test cases (which could be > all test cases) because > > test_suite_class: type[TestSuite] > test_cases: list[MethodType] > + req_capabilities: set[NicCapability] = field(default_factory=set, > init=False) > + > + def __post_init__(self): > + """Gather the required capabilities of the test suite and all test > cases.""" > + for test_object in [self.test_suite_class] + self.test_cases: > + test_object.skip = False > + if hasattr(test_object, "req_capa"): > + self.req_capabilities.update(test_object.req_capa) > > def create_config(self) -> TestSuiteConfig: > """Generate a :class:`TestSuiteConfig` from the stored test suite > with test cases. > @@ -76,6 +86,47 @@ def create_config(self) -> TestSuiteConfig: > test_cases=[test_case.__name__ for test_case in self.test_cases], > ) > > + def mark_skip(self, supported_capabilities: MutableSet[NicCapability]) > -> None: > + """Mark the test suite and test cases to be skipped. > + > + The mark is applied is object to be skipped requires any > capabilities and at least one of "The mark is applied if the object to be skipped." > + them is not among `capabilities`. Maybe change 'capabilities' to 'supported_capabilities.' Unless I'm just misunderstanding the comment. > + > + Args: > + supported_capabilities: The supported capabilities. > + """ > + for test_object in [self.test_suite_class] + self.test_cases: > + if set(getattr(test_object, "req_capa", [])) - > supported_capabilities: > + test_object.skip = True > + > + @staticmethod > + def should_not_be_skipped(test_object: Union[type[TestSuite] | > MethodType]) -> bool: > + """Figure out whether `test_object` should be skipped. > + > + If `test_object` is a :class:`TestSuite`, its test cases are not > checked, > + only the class itself. > + > + Args: > + test_object: The test suite or test case to be inspected. > + > + Returns: > + :data:`True` if the test suite or test case should be skipped, > :data:`False` otherwise. > + """ > + return not getattr(test_object, "skip", False) > + > + def __bool__(self) -> bool: > + """The truth value is determined by whether the test suite should be > run. > + > + Returns: > + :data:`False` if the test suite should be skipped, :data:`True` > otherwise. > + """ > + found_test_case_to_run = False > + for test_case in self.test_cases: > + if self.should_not_be_skipped(test_case): > + found_test_case_to_run = True > + break > + return found_test_case_to_run and > self.should_not_be_skipped(self.test_suite_class) > + > > class Result(Enum): > """The possible states that a setup, a teardown or a test case may end > up in.""" > @@ -170,12 +221,13 @@ def update_setup(self, result: Result, error: Exception > | None = None) -> None: > self.setup_result.result = result > self.setup_result.error = error > > - if result in [Result.BLOCK, Result.ERROR, Result.FAIL]: > - self.update_teardown(Result.BLOCK) > - self._block_result() > + if result != Result.PASS: > + result_to_mark = Result.BLOCK if result != Result.SKIP else > Result.SKIP > + self.update_teardown(result_to_mark) > + self._mark_results(result_to_mark) > > - def _block_result(self) -> None: > - r"""Mark the result as :attr:`~Result.BLOCK`\ed. > + def _mark_results(self, result) -> None: > + """Mark the result as well as the child result as `result`. > > The blocking of child results should be done in overloaded methods. > """ > @@ -390,11 +442,11 @@ def add_sut_info(self, sut_info: NodeInfo) -> None: > self.sut_os_version = sut_info.os_version > self.sut_kernel_version = sut_info.kernel_version > > - def _block_result(self) -> None: > - r"""Mark the result as :attr:`~Result.BLOCK`\ed.""" > + def _mark_results(self, result) -> None: > + """Mark the result as well as the child result as `result`.""" > for build_target in self._config.build_targets: > child_result = self.add_build_target(build_target) > - child_result.update_setup(Result.BLOCK) > + child_result.update_setup(result) > > > class BuildTargetResult(BaseResult): > @@ -464,11 +516,11 @@ def add_build_target_info(self, versions: > BuildTargetInfo) -> None: > self.compiler_version = versions.compiler_version > self.dpdk_version = versions.dpdk_version > > - def _block_result(self) -> None: > - r"""Mark the result as :attr:`~Result.BLOCK`\ed.""" > + def _mark_results(self, result) -> None: > + """Mark the result as well as the child result as `result`.""" > for test_suite_with_cases in self._test_suites_with_cases: > child_result = self.add_test_suite(test_suite_with_cases) > - child_result.update_setup(Result.BLOCK) > + child_result.update_setup(result) > > > class TestSuiteResult(BaseResult): > @@ -508,11 +560,11 @@ def add_test_case(self, test_case_name: str) -> > "TestCaseResult": > self.child_results.append(result) > return result > > - def _block_result(self) -> None: > - r"""Mark the result as :attr:`~Result.BLOCK`\ed.""" > + def _mark_results(self, result) -> None: > + """Mark the result as well as the child result as `result`.""" > for test_case_method in self._test_suite_with_cases.test_cases: > child_result = self.add_test_case(test_case_method.__name__) > - child_result.update_setup(Result.BLOCK) > + child_result.update_setup(result) > > > class TestCaseResult(BaseResult, FixtureResult): > @@ -566,9 +618,9 @@ def add_stats(self, statistics: "Statistics") -> None: > """ > statistics += self.result > > - def _block_result(self) -> None: > - r"""Mark the result as :attr:`~Result.BLOCK`\ed.""" > - self.update(Result.BLOCK) > + def _mark_results(self, result) -> None: > + r"""Mark the result as `result`.""" > + self.update(result) > > def __bool__(self) -> bool: > """The test case passed only if setup, teardown and the test case > itself passed.""" > diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py > index 9c3b516002..07cdd294b9 100644 > --- a/dts/framework/test_suite.py > +++ b/dts/framework/test_suite.py > @@ -13,6 +13,7 @@ > * Test case verification. > """ > > +from collections.abc import Callable > from ipaddress import IPv4Interface, IPv6Interface, ip_interface > from typing import ClassVar, Union > > @@ -20,6 +21,8 @@ > from scapy.layers.l2 import Ether # type: ignore[import] > from scapy.packet import Packet, Padding # type: ignore[import] > > +from framework.remote_session import NicCapability > + > from .exception import TestCaseVerifyError > from .logger import DTSLogger, get_dts_logger > from .testbed_model import Port, PortLink, SutNode, TGNode > @@ -62,6 +65,7 @@ class TestSuite(object): > #: Whether the test suite is blocking. A failure of a blocking test suite > #: will block the execution of all subsequent test suites in the current > build target. > is_blocking: ClassVar[bool] = False > + skip: bool > _logger: DTSLogger > _port_links: list[PortLink] > _sut_port_ingress: Port > @@ -89,6 +93,7 @@ def __init__( > """ > self.sut_node = sut_node > self.tg_node = tg_node > + self.skip = False > self._logger = get_dts_logger(self.__class__.__name__) > self._port_links = [] > self._process_links() > @@ -360,3 +365,23 @@ def _verify_l3_packet(self, received_packet: IP, > expected_packet: IP) -> bool: > if received_packet.src != expected_packet.src or received_packet.dst > != expected_packet.dst: > return False > return True > + > + > +def requires(capability: NicCapability) -> Callable: > + """A decorator that marks the decorated test case or test suite as one > to be skipped. I think there might be an error here. Are you trying to say "A decorator that marks the test case/test suite as having additional requirements" ? > + > + Args: > + The capability that's required by the decorated test case or test > suite. > + > + Returns: > + The decorated function. > + """ > + > + def add_req_capa(func) -> Callable: > + if hasattr(func, "req_capa"): > + func.req_capa.append(capability) > + else: > + func.req_capa = [capability] > + return func > + > + return add_req_capa > diff --git a/dts/framework/testbed_model/sut_node.py > b/dts/framework/testbed_model/sut_node.py > index 97aa26d419..1fb536735d 100644 > --- a/dts/framework/testbed_model/sut_node.py > +++ b/dts/framework/testbed_model/sut_node.py > @@ -15,7 +15,7 @@ > import tarfile > import time > from pathlib import PurePath > -from typing import Type > +from typing import Iterable, Type > > from framework.config import ( > BuildTargetConfiguration, > @@ -23,7 +23,7 @@ > NodeInfo, > SutNodeConfiguration, > ) > -from framework.remote_session import CommandResult > +from framework.remote_session import CommandResult, NicCapability, > TestPmdShell > from framework.settings import SETTINGS > from framework.utils import MesonArgs > > @@ -228,6 +228,27 @@ def get_build_target_info(self) -> BuildTargetInfo: > def _guess_dpdk_remote_dir(self) -> PurePath: > return self.main_session.guess_dpdk_remote_dir(self._remote_tmp_dir) > > + def get_supported_capabilities( > + self, capabilities: Iterable[NicCapability] > + ) -> set[NicCapability]: > + """Get the supported capabilities of the current NIC from > `capabilities`. I wonder if it might be more readable to change the 'capabilities' variable to 'required_capabilities' or something along those lines. Although I do understand why you have selected 'capabilities' in the first place if the capabilities being passed in may not necessarily be required capabilities 100% of the time. > + > + Args: > + capabilities: The capabilities to verify. > + > + Returns: > + The set of supported capabilities of the current NIC. > + """ > + supported_capas: set[NicCapability] = set() > + unsupported_capas: set[NicCapability] = set() > + self._logger.debug(f"Checking which capabilities from {capabilities} > NIC are supported.") > + testpmd_shell = self.create_interactive_shell(TestPmdShell, > privileged=True) > + for capability in capabilities: > + if capability not in supported_capas or capability not in > unsupported_capas: > + capability.value(testpmd_shell, supported_capas, > unsupported_capas) > + del testpmd_shell > + return supported_capas > + > def _set_up_build_target(self, build_target_config: > BuildTargetConfiguration) -> None: > """Setup DPDK on the SUT node. > > diff --git a/dts/tests/TestSuite_hello_world.py > b/dts/tests/TestSuite_hello_world.py > index fd7ff1534d..31b1564582 100644 > --- a/dts/tests/TestSuite_hello_world.py > +++ b/dts/tests/TestSuite_hello_world.py > @@ -7,7 +7,8 @@ > No other EAL parameters apart from cores are used. > """ > > -from framework.test_suite import TestSuite > +from framework.remote_session import NicCapability > +from framework.test_suite import TestSuite, requires > from framework.testbed_model import ( > LogicalCoreCount, > LogicalCoreCountFilter, > @@ -26,6 +27,7 @@ def set_up_suite(self) -> None: > """ > self.app_helloworld_path = self.sut_node.build_dpdk_app("helloworld") > > + @requires(NicCapability.scattered_rx) > def test_hello_world_single_core(self) -> None: > """Single core test case. > > -- > 2.34.1 > The above comments are basically just nitpicks, but if nothing else, I figured I'd bring it to your attention.