On Wed, Jul 12, 2023 at 10:00 PM Jeremy Spewock <jspew...@iol.unh.edu> wrote: > > This fails to apply because it modifies some of the same files that the > previous DTS patches that were applied today also change. These changes only > modify things in dts/ which isn't currently being used in CI testing so there > wouldn't be any breaking changes for these builds regardless. Apologies for > my lack of knowledge on how this is handled, but are these conflicts > something that I need to fix in my patch or are instead handled by > maintainers who are applying and merging the patches on these release > candidates? >
Send a new version if you have the bandwidth, it will take some work off maintainers, who are very busy. > Thanks, > Jeremy > > On Wed, Jul 12, 2023 at 3:21 PM <jspew...@iol.unh.edu> wrote: >> >> From: Jeremy Spewock <jspew...@iol.unh.edu> >> >> Adds a new test suite for running smoke tests that verify general >> configuration aspects of the system under test. If any of these tests >> fail, the DTS execution terminates as part of a "fail-fast" model. >> >> Signed-off-by: Jeremy Spewock <jspew...@iol.unh.edu> >> --- >> dts/conf.yaml | 17 +- >> dts/framework/config/__init__.py | 105 +++++++++-- >> dts/framework/config/conf_yaml_schema.json | 142 +++++++++++++- >> dts/framework/dts.py | 87 ++++++--- >> dts/framework/exception.py | 12 ++ >> dts/framework/remote_session/__init__.py | 11 +- >> dts/framework/remote_session/os_session.py | 53 +++++- >> dts/framework/remote_session/posix_session.py | 29 ++- >> .../remote_session/remote/__init__.py | 10 + >> .../remote/interactive_remote_session.py | 82 ++++++++ >> .../remote/interactive_shell.py | 75 ++++++++ >> .../remote_session/remote/testpmd_shell.py | 75 ++++++++ >> dts/framework/test_result.py | 37 +++- >> dts/framework/test_suite.py | 10 +- >> dts/framework/testbed_model/node.py | 2 + >> dts/framework/testbed_model/sut_node.py | 176 +++++++++++++----- >> dts/framework/utils.py | 2 + >> dts/tests/TestSuite_smoke_tests.py | 113 +++++++++++ >> 18 files changed, 941 insertions(+), 97 deletions(-) >> create mode 100644 >> dts/framework/remote_session/remote/interactive_remote_session.py >> create mode 100644 dts/framework/remote_session/remote/interactive_shell.py >> create mode 100644 dts/framework/remote_session/remote/testpmd_shell.py >> create mode 100644 dts/tests/TestSuite_smoke_tests.py >> >> diff --git a/dts/conf.yaml b/dts/conf.yaml >> index a9bd8a3e..c0be7848 100644 >> --- a/dts/conf.yaml >> +++ b/dts/conf.yaml >> @@ -10,9 +10,13 @@ executions: >> compiler_wrapper: ccache >> perf: false >> func: true >> + skip_smoke_tests: false # optional flag that allow you to skip smoke >> tests >> test_suites: >> - hello_world >> - system_under_test: "SUT 1" >> + system_under_test: >> + node_name: "SUT 1" >> + vdevs: # optional; if removed, vdevs won't be used in the execution >> + - "crypto_openssl" >> nodes: >> - name: "SUT 1" >> hostname: sut1.change.me.localhost >> @@ -25,3 +29,14 @@ nodes: >> hugepages: # optional; if removed, will use system hugepage >> configuration >> amount: 256 >> force_first_numa: false >> + ports: >> + - pci: "0000:00:08.0" >> + os_driver_for_dpdk: vfio-pci # OS driver that DPDK will use >> + os_driver: i40e >> + peer_node: "TG 1" >> + peer_pci: "0000:00:08.0" >> + - pci: "0000:00:08.1" >> + os_driver_for_dpdk: vfio-pci >> + os_driver: i40e >> + peer_node: "TG 1" >> + peer_pci: "0000:00:08.1" >> diff --git a/dts/framework/config/__init__.py >> b/dts/framework/config/__init__.py >> index ebb0823f..9e144748 100644 >> --- a/dts/framework/config/__init__.py >> +++ b/dts/framework/config/__init__.py >> @@ -12,6 +12,7 @@ >> import pathlib >> from dataclasses import dataclass >> from enum import Enum, auto, unique >> +from pathlib import PurePath >> from typing import Any, TypedDict >> >> import warlock # type: ignore >> @@ -72,6 +73,20 @@ class HugepageConfiguration: >> force_first_numa: bool >> >> >> +@dataclass(slots=True, frozen=True) >> +class PortConfig: >> + node: str >> + pci: str >> + os_driver_for_dpdk: str >> + os_driver: str >> + peer_node: str >> + peer_pci: str >> + >> + @staticmethod >> + def from_dict(node: str, d: dict) -> "PortConfig": >> + return PortConfig(node=node, **d) >> + >> + >> @dataclass(slots=True, frozen=True) >> class NodeConfiguration: >> name: str >> @@ -84,6 +99,7 @@ class NodeConfiguration: >> use_first_core: bool >> memory_channels: int >> hugepages: HugepageConfiguration | None >> + ports: list[PortConfig] >> >> @staticmethod >> def from_dict(d: dict) -> "NodeConfiguration": >> @@ -92,19 +108,36 @@ def from_dict(d: dict) -> "NodeConfiguration": >> if "force_first_numa" not in hugepage_config: >> hugepage_config["force_first_numa"] = False >> hugepage_config = HugepageConfiguration(**hugepage_config) >> + common_config = { >> + "name": d["name"], >> + "hostname": d["hostname"], >> + "user": d["user"], >> + "password": d.get("password"), >> + "arch": Architecture(d["arch"]), >> + "os": OS(d["os"]), >> + "lcores": d.get("lcores", "1"), >> + "use_first_core": d.get("use_first_core", False), >> + "memory_channels": d.get("memory_channels", 1), >> + "hugepages": hugepage_config, >> + "ports": [PortConfig.from_dict(d["name"], port) for port in >> d["ports"]], >> + } >> + >> + return NodeConfiguration(**common_config) >> >> - return NodeConfiguration( >> - name=d["name"], >> - hostname=d["hostname"], >> - user=d["user"], >> - password=d.get("password"), >> - arch=Architecture(d["arch"]), >> - os=OS(d["os"]), >> - lcores=d.get("lcores", "1"), >> - use_first_core=d.get("use_first_core", False), >> - memory_channels=d.get("memory_channels", 1), >> - hugepages=hugepage_config, >> - ) >> + >> +@dataclass(slots=True, frozen=True) >> +class NodeInfo: >> + """Class to hold important versions within the node. >> + >> + This class, unlike the NodeConfiguration class, cannot be generated at >> the start. >> + This is because we need to initialize a connection with the node before >> we can >> + collect the information needed in this class. Therefore, it cannot be a >> part of >> + the configuration class above. >> + """ >> + >> + os_name: str >> + os_version: str >> + kernel_version: str >> >> >> @dataclass(slots=True, frozen=True) >> @@ -128,6 +161,18 @@ def from_dict(d: dict) -> "BuildTargetConfiguration": >> ) >> >> >> +@dataclass(slots=True, frozen=True) >> +class BuildTargetInfo: >> + """Class to hold important versions within the build target. >> + >> + This is very similar to the NodeInfo class, it just instead holds >> information >> + for the build target. >> + """ >> + >> + dpdk_version: str >> + compiler_version: str >> + >> + >> class TestSuiteConfigDict(TypedDict): >> suite: str >> cases: list[str] >> @@ -157,6 +202,8 @@ class ExecutionConfiguration: >> func: bool >> test_suites: list[TestSuiteConfig] >> system_under_test: NodeConfiguration >> + vdevs: list[str] >> + skip_smoke_tests: bool >> >> @staticmethod >> def from_dict(d: dict, node_map: dict) -> "ExecutionConfiguration": >> @@ -166,15 +213,20 @@ def from_dict(d: dict, node_map: dict) -> >> "ExecutionConfiguration": >> test_suites: list[TestSuiteConfig] = list( >> map(TestSuiteConfig.from_dict, d["test_suites"]) >> ) >> - sut_name = d["system_under_test"] >> + sut_name = d["system_under_test"]["node_name"] >> + skip_smoke_tests = d.get("skip_smoke_tests", False) >> assert sut_name in node_map, f"Unknown SUT {sut_name} in execution >> {d}" >> - >> + vdevs = ( >> + d["system_under_test"]["vdevs"] if "vdevs" in >> d["system_under_test"] else [] >> + ) >> return ExecutionConfiguration( >> build_targets=build_targets, >> perf=d["perf"], >> func=d["func"], >> + skip_smoke_tests=skip_smoke_tests, >> test_suites=test_suites, >> system_under_test=node_map[sut_name], >> + vdevs=vdevs, >> ) >> >> >> @@ -221,3 +273,28 @@ def load_config() -> Configuration: >> >> >> CONFIGURATION = load_config() >> + >> + >> +@unique >> +class InteractiveApp(Enum): >> + """An enum that represents different supported interactive applications. >> + >> + The values in this enum must all be set to objects that have a key >> called >> + "default_path" where "default_path" represents a PurePath object for >> the path >> + to the application. This default path will be passed into the handler >> class >> + for the application so that it can start the application. >> + """ >> + >> + testpmd = {"default_path": PurePath("app", "dpdk-testpmd")} >> + >> + @property >> + def path(self) -> PurePath: >> + """Default path of the application. >> + >> + For DPDK apps, this will be appended to the DPDK build directory. >> + """ >> + return self.value["default_path"] >> + >> + @path.setter >> + def path(self, path: PurePath) -> None: >> + self.value["default_path"] = path >> diff --git a/dts/framework/config/conf_yaml_schema.json >> b/dts/framework/config/conf_yaml_schema.json >> index ca2d4a1e..61f52b43 100644 >> --- a/dts/framework/config/conf_yaml_schema.json >> +++ b/dts/framework/config/conf_yaml_schema.json >> @@ -6,6 +6,76 @@ >> "type": "string", >> "description": "A unique identifier for a node" >> }, >> + "NIC": { >> + "type": "string", >> + "enum": [ >> + "ALL", >> + "ConnectX3_MT4103", >> + "ConnectX4_LX_MT4117", >> + "ConnectX4_MT4115", >> + "ConnectX5_MT4119", >> + "ConnectX5_MT4121", >> + "I40E_10G-10G_BASE_T_BC", >> + "I40E_10G-10G_BASE_T_X722", >> + "I40E_10G-SFP_X722", >> + "I40E_10G-SFP_XL710", >> + "I40E_10G-X722_A0", >> + "I40E_1G-1G_BASE_T_X722", >> + "I40E_25G-25G_SFP28", >> + "I40E_40G-QSFP_A", >> + "I40E_40G-QSFP_B", >> + "IAVF-ADAPTIVE_VF", >> + "IAVF-VF", >> + "IAVF_10G-X722_VF", >> + "ICE_100G-E810C_QSFP", >> + "ICE_25G-E810C_SFP", >> + "ICE_25G-E810_XXV_SFP", >> + "IGB-I350_VF", >> + "IGB_1G-82540EM", >> + "IGB_1G-82545EM_COPPER", >> + "IGB_1G-82571EB_COPPER", >> + "IGB_1G-82574L", >> + "IGB_1G-82576", >> + "IGB_1G-82576_QUAD_COPPER", >> + "IGB_1G-82576_QUAD_COPPER_ET2", >> + "IGB_1G-82580_COPPER", >> + "IGB_1G-I210_COPPER", >> + "IGB_1G-I350_COPPER", >> + "IGB_1G-I354_SGMII", >> + "IGB_1G-PCH_LPTLP_I218_LM", >> + "IGB_1G-PCH_LPTLP_I218_V", >> + "IGB_1G-PCH_LPT_I217_LM", >> + "IGB_1G-PCH_LPT_I217_V", >> + "IGB_2.5G-I354_BACKPLANE_2_5GBPS", >> + "IGC-I225_LM", >> + "IGC-I226_LM", >> + "IXGBE_10G-82599_SFP", >> + "IXGBE_10G-82599_SFP_SF_QP", >> + "IXGBE_10G-82599_T3_LOM", >> + "IXGBE_10G-82599_VF", >> + "IXGBE_10G-X540T", >> + "IXGBE_10G-X540_VF", >> + "IXGBE_10G-X550EM_A_SFP", >> + "IXGBE_10G-X550EM_X_10G_T", >> + "IXGBE_10G-X550EM_X_SFP", >> + "IXGBE_10G-X550EM_X_VF", >> + "IXGBE_10G-X550T", >> + "IXGBE_10G-X550_VF", >> + "brcm_57414", >> + "brcm_P2100G", >> + "cavium_0011", >> + "cavium_a034", >> + "cavium_a063", >> + "cavium_a064", >> + "fastlinq_ql41000", >> + "fastlinq_ql41000_vf", >> + "fastlinq_ql45000", >> + "fastlinq_ql45000_vf", >> + "hi1822", >> + "virtio" >> + ] >> + }, >> + >> "ARCH": { >> "type": "string", >> "enum": [ >> @@ -94,6 +164,19 @@ >> "amount" >> ] >> }, >> + "pci_address": { >> + "type": "string", >> + "pattern": "^[\\da-fA-F]{4}:[\\da-fA-F]{2}:[\\da-fA-F]{2}.\\d:?\\w*$" >> + }, >> + "port_peer_address": { >> + "description": "Peer is a TRex port, and IXIA port or a PCI address", >> + "oneOf": [ >> + { >> + "description": "PCI peer port", >> + "$ref": "#/definitions/pci_address" >> + } >> + ] >> + }, >> "test_suite": { >> "type": "string", >> "enum": [ >> @@ -165,6 +248,44 @@ >> }, >> "hugepages": { >> "$ref": "#/definitions/hugepages" >> + }, >> + "ports": { >> + "type": "array", >> + "items": { >> + "type": "object", >> + "description": "Each port should be described on both sides >> of the connection. This makes configuration slightly more verbose but >> greatly simplifies implementation. If there are an inconsistencies, then DTS >> will not run until that issue is fixed. An example inconsistency would be >> port 1, node 1 says it is connected to port 1, node 2, but port 1, node 2 >> says it is connected to port 2, node 1.", >> + "properties": { >> + "pci": { >> + "$ref": "#/definitions/pci_address", >> + "description": "The local PCI address of the port" >> + }, >> + "os_driver_for_dpdk": { >> + "type": "string", >> + "description": "The driver that the kernel should bind >> this device to for DPDK to use it. (ex: vfio-pci)" >> + }, >> + "os_driver": { >> + "type": "string", >> + "description": "The driver normally used by this port >> (ex: i40e)" >> + }, >> + "peer_node": { >> + "type": "string", >> + "description": "The name of the node the peer port is on" >> + }, >> + "peer_pci": { >> + "$ref": "#/definitions/pci_address", >> + "description": "The PCI address of the peer port" >> + } >> + }, >> + "additionalProperties": false, >> + "required": [ >> + "pci", >> + "os_driver_for_dpdk", >> + "os_driver", >> + "peer_node", >> + "peer_pci" >> + ] >> + }, >> + "minimum": 1 >> } >> }, >> "additionalProperties": false, >> @@ -211,8 +332,27 @@ >> ] >> } >> }, >> + "skip_smoke_tests": { >> + "description": "Optional field that allows you to skip smoke >> testing", >> + "type": "boolean" >> + }, >> "system_under_test": { >> - "$ref": "#/definitions/node_name" >> + "type":"object", >> + "properties": { >> + "node_name": { >> + "$ref": "#/definitions/node_name" >> + }, >> + "vdevs": { >> + "description": "Opentional list of names of vdevs to be >> used in execution", >> + "type": "array", >> + "items": { >> + "type": "string" >> + } >> + } >> + }, >> + "required": [ >> + "node_name" >> + ] >> } >> }, >> "additionalProperties": false, >> diff --git a/dts/framework/dts.py b/dts/framework/dts.py >> index 05022845..7b09d8fb 100644 >> --- a/dts/framework/dts.py >> +++ b/dts/framework/dts.py >> @@ -5,7 +5,13 @@ >> >> import sys >> >> -from .config import CONFIGURATION, BuildTargetConfiguration, >> ExecutionConfiguration >> +from .config import ( >> + CONFIGURATION, >> + BuildTargetConfiguration, >> + ExecutionConfiguration, >> + TestSuiteConfig, >> +) >> +from .exception import BlockingTestSuiteError >> from .logger import DTSLOG, getLogger >> from .test_result import BuildTargetResult, DTSResult, ExecutionResult, >> Result >> from .test_suite import get_test_suites >> @@ -82,7 +88,7 @@ def _run_execution( >> running all build targets in the given execution. >> """ >> dts_logger.info(f"Running execution with SUT >> '{execution.system_under_test.name}'.") >> - execution_result = result.add_execution(sut_node.config) >> + execution_result = result.add_execution(sut_node.config, >> sut_node.node_info) >> >> try: >> sut_node.set_up_execution(execution) >> @@ -118,14 +124,15 @@ def _run_build_target( >> >> try: >> sut_node.set_up_build_target(build_target) >> - result.dpdk_version = sut_node.dpdk_version >> + # result.dpdk_version = sut_node.dpdk_version >> + >> build_target_result.add_build_target_versions(sut_node.get_build_target_info()) >> build_target_result.update_setup(Result.PASS) >> except Exception as e: >> dts_logger.exception("Build target setup failed.") >> build_target_result.update_setup(Result.FAIL, e) >> >> else: >> - _run_suites(sut_node, execution, build_target_result) >> + _run_all_suites(sut_node, execution, build_target_result) >> >> finally: >> try: >> @@ -136,7 +143,7 @@ def _run_build_target( >> build_target_result.update_teardown(Result.FAIL, e) >> >> >> -def _run_suites( >> +def _run_all_suites( >> sut_node: SutNode, >> execution: ExecutionConfiguration, >> build_target_result: BuildTargetResult, >> @@ -146,27 +153,61 @@ def _run_suites( >> with possibly only a subset of test cases. >> If no subset is specified, run all test cases. >> """ >> + end_build_target = False >> + if not execution.skip_smoke_tests: >> + execution.test_suites[:0] = >> [TestSuiteConfig.from_dict("smoke_tests")] >> for test_suite_config in execution.test_suites: >> try: >> - full_suite_path = >> f"tests.TestSuite_{test_suite_config.test_suite}" >> - test_suite_classes = get_test_suites(full_suite_path) >> - suites_str = ", ".join((x.__name__ for x in test_suite_classes)) >> - dts_logger.debug( >> - f"Found test suites '{suites_str}' in '{full_suite_path}'." >> + _run_single_suite( >> + sut_node, execution, build_target_result, test_suite_config >> ) >> - except Exception as e: >> - dts_logger.exception("An error occurred when searching for test >> suites.") >> - result.update_setup(Result.ERROR, e) >> - >> - else: >> - for test_suite_class in test_suite_classes: >> - test_suite = test_suite_class( >> - sut_node, >> - test_suite_config.test_cases, >> - execution.func, >> - build_target_result, >> - ) >> - test_suite.run() >> + except BlockingTestSuiteError as e: >> + dts_logger.exception( >> + f"An error occurred within {test_suite_config.test_suite}. " >> + "Skipping build target..." >> + ) >> + result.add_error(e) >> + end_build_target = True >> + # if a blocking test failed and we need to bail out of suite >> executions >> + if end_build_target: >> + break >> + >> + >> +def _run_single_suite( >> + sut_node: SutNode, >> + execution: ExecutionConfiguration, >> + build_target_result: BuildTargetResult, >> + test_suite_config: TestSuiteConfig, >> +) -> None: >> + """Runs a single test suite. >> + >> + Args: >> + sut_node: Node to run tests on. >> + execution: Execution the test case belongs to. >> + build_target_result: Build target configuration test case is run on >> + test_suite_config: Test suite configuration >> + >> + Raises: >> + BlockingTestSuiteError: If a test suite that was marked as blocking >> fails. >> + """ >> + try: >> + full_suite_path = f"tests.TestSuite_{test_suite_config.test_suite}" >> + test_suite_classes = get_test_suites(full_suite_path) >> + suites_str = ", ".join((x.__name__ for x in test_suite_classes)) >> + dts_logger.debug(f"Found test suites '{suites_str}' in >> '{full_suite_path}'.") >> + except Exception as e: >> + dts_logger.exception("An error occurred when searching for test >> suites.") >> + result.update_setup(Result.ERROR, e) >> + >> + else: >> + for test_suite_class in test_suite_classes: >> + test_suite = test_suite_class( >> + sut_node, >> + test_suite_config.test_cases, >> + execution.func, >> + build_target_result, >> + ) >> + test_suite.run() >> >> >> def _exit_dts() -> None: >> diff --git a/dts/framework/exception.py b/dts/framework/exception.py >> index ca353d98..dfb12df4 100644 >> --- a/dts/framework/exception.py >> +++ b/dts/framework/exception.py >> @@ -25,6 +25,7 @@ class ErrorSeverity(IntEnum): >> SSH_ERR = 4 >> DPDK_BUILD_ERR = 10 >> TESTCASE_VERIFY_ERR = 20 >> + BLOCKING_TESTSUITE_ERR = 25 >> >> >> class DTSError(Exception): >> @@ -144,3 +145,14 @@ def __init__(self, value: str): >> >> def __str__(self) -> str: >> return repr(self.value) >> + >> + >> +class BlockingTestSuiteError(DTSError): >> + suite_name: str >> + severity: ClassVar[ErrorSeverity] = ErrorSeverity.BLOCKING_TESTSUITE_ERR >> + >> + def __init__(self, suite_name: str) -> None: >> + self.suite_name = suite_name >> + >> + def __str__(self) -> str: >> + return f"Blocking suite {self.suite_name} failed." >> diff --git a/dts/framework/remote_session/__init__.py >> b/dts/framework/remote_session/__init__.py >> index ee221503..2c408c25 100644 >> --- a/dts/framework/remote_session/__init__.py >> +++ b/dts/framework/remote_session/__init__.py >> @@ -1,5 +1,6 @@ >> # SPDX-License-Identifier: BSD-3-Clause >> # Copyright(c) 2023 PANTHEON.tech s.r.o. >> +# Copyright(c) 2023 University of New Hampshire >> >> """ >> The package provides modules for managing remote connections to a remote >> host (node), >> @@ -17,7 +18,15 @@ >> >> from .linux_session import LinuxSession >> from .os_session import OSSession >> -from .remote import CommandResult, RemoteSession, SSHSession >> +from .remote import ( >> + CommandResult, >> + InteractiveRemoteSession, >> + InteractiveShell, >> + RemoteSession, >> + SSHSession, >> + TestPmdDevice, >> + TestPmdShell, >> +) >> >> >> def create_session( >> diff --git a/dts/framework/remote_session/os_session.py >> b/dts/framework/remote_session/os_session.py >> index 4c48ae25..48f05c44 100644 >> --- a/dts/framework/remote_session/os_session.py >> +++ b/dts/framework/remote_session/os_session.py >> @@ -5,14 +5,22 @@ >> from abc import ABC, abstractmethod >> from collections.abc import Iterable >> from pathlib import PurePath >> +from typing import Union >> >> -from framework.config import Architecture, NodeConfiguration >> +from framework.config import Architecture, InteractiveApp, >> NodeConfiguration, NodeInfo >> from framework.logger import DTSLOG >> +from framework.remote_session.remote import InteractiveShell, TestPmdShell >> from framework.settings import SETTINGS >> from framework.testbed_model import LogicalCore >> from framework.utils import EnvVarsDict, MesonArgs >> >> -from .remote import CommandResult, RemoteSession, create_remote_session >> +from .remote import ( >> + CommandResult, >> + InteractiveRemoteSession, >> + RemoteSession, >> + create_interactive_session, >> + create_remote_session, >> +) >> >> >> class OSSession(ABC): >> @@ -26,6 +34,7 @@ class OSSession(ABC): >> name: str >> _logger: DTSLOG >> remote_session: RemoteSession >> + interactive_session: InteractiveRemoteSession >> >> def __init__( >> self, >> @@ -37,6 +46,7 @@ def __init__( >> self.name = name >> self._logger = logger >> self.remote_session = create_remote_session(node_config, name, >> logger) >> + self.interactive_session = create_interactive_session(node_config, >> name, logger) >> >> def close(self, force: bool = False) -> None: >> """ >> @@ -64,6 +74,33 @@ def send_command( >> """ >> return self.remote_session.send_command(command, timeout, verify, >> env) >> >> + def create_interactive_shell( >> + self, >> + shell_type: InteractiveApp, >> + path_to_app: PurePath, >> + eal_parameters: str, >> + timeout: float, >> + ) -> Union[InteractiveShell, TestPmdShell]: >> + """ >> + See "create_interactive_shell" in SutNode >> + """ >> + match (shell_type): >> + case InteractiveApp.testpmd: >> + return TestPmdShell( >> + self.interactive_session.session, >> + self._logger, >> + path_to_app, >> + timeout=timeout, >> + eal_flags=eal_parameters, >> + ) >> + case _: >> + self._logger.info( >> + f"Unhandled app type {shell_type.name}, defaulting to >> shell." >> + ) >> + return InteractiveShell( >> + self.interactive_session.session, self._logger, >> path_to_app, timeout >> + ) >> + >> @abstractmethod >> def guess_dpdk_remote_dir(self, remote_dir) -> PurePath: >> """ >> @@ -173,3 +210,15 @@ def setup_hugepages(self, hugepage_amount: int, >> force_first_numa: bool) -> None: >> if needed and mount the hugepages if needed. >> If force_first_numa is True, configure hugepages just on the first >> socket. >> """ >> + >> + @abstractmethod >> + def get_compiler_version(self, compiler_name: str) -> str: >> + """ >> + Get installed version of compiler used for DPDK >> + """ >> + >> + @abstractmethod >> + def get_node_info(self) -> NodeInfo: >> + """ >> + Collect information about the node >> + """ >> diff --git a/dts/framework/remote_session/posix_session.py >> b/dts/framework/remote_session/posix_session.py >> index d38062e8..4e419877 100644 >> --- a/dts/framework/remote_session/posix_session.py >> +++ b/dts/framework/remote_session/posix_session.py >> @@ -6,7 +6,7 @@ >> from collections.abc import Iterable >> from pathlib import PurePath, PurePosixPath >> >> -from framework.config import Architecture >> +from framework.config import Architecture, NodeInfo >> from framework.exception import DPDKBuildError, RemoteCommandExecutionError >> from framework.settings import SETTINGS >> from framework.utils import EnvVarsDict, MesonArgs >> @@ -219,3 +219,30 @@ def _remove_dpdk_runtime_dirs( >> >> def get_dpdk_file_prefix(self, dpdk_prefix) -> str: >> return "" >> + >> + def get_compiler_version(self, compiler_name: str) -> str: >> + match compiler_name: >> + case "gcc": >> + return self.send_command( >> + f"{compiler_name} --version", SETTINGS.timeout >> + ).stdout.split("\n")[0] >> + case "clang": >> + return self.send_command( >> + f"{compiler_name} --version", SETTINGS.timeout >> + ).stdout.split("\n")[0] >> + case "msvc": >> + return self.send_command("cl", SETTINGS.timeout).stdout >> + case "icc": >> + return self.send_command(f"{compiler_name} -V", >> SETTINGS.timeout).stdout >> + case _: >> + raise ValueError(f"Unknown compiler {compiler_name}") >> + >> + def get_node_info(self) -> NodeInfo: >> + os_release_info = self.send_command( >> + "awk -F= '$1 ~ /^NAME$|^VERSION$/ {print $2}' /etc/os-release", >> + SETTINGS.timeout, >> + ).stdout.split("\n") >> + kernel_version = self.send_command("uname -r", >> SETTINGS.timeout).stdout >> + return NodeInfo( >> + os_release_info[0].strip(), os_release_info[1].strip(), >> kernel_version >> + ) >> diff --git a/dts/framework/remote_session/remote/__init__.py >> b/dts/framework/remote_session/remote/__init__.py >> index 8a151221..03fd309f 100644 >> --- a/dts/framework/remote_session/remote/__init__.py >> +++ b/dts/framework/remote_session/remote/__init__.py >> @@ -1,16 +1,26 @@ >> # SPDX-License-Identifier: BSD-3-Clause >> # Copyright(c) 2023 PANTHEON.tech s.r.o. >> +# Copyright(c) 2023 University of New Hampshire >> >> # pylama:ignore=W0611 >> >> from framework.config import NodeConfiguration >> from framework.logger import DTSLOG >> >> +from .interactive_remote_session import InteractiveRemoteSession >> +from .interactive_shell import InteractiveShell >> from .remote_session import CommandResult, RemoteSession >> from .ssh_session import SSHSession >> +from .testpmd_shell import TestPmdDevice, TestPmdShell >> >> >> def create_remote_session( >> node_config: NodeConfiguration, name: str, logger: DTSLOG >> ) -> RemoteSession: >> return SSHSession(node_config, name, logger) >> + >> + >> +def create_interactive_session( >> + node_config: NodeConfiguration, name: str, logger: DTSLOG >> +) -> InteractiveRemoteSession: >> + return InteractiveRemoteSession(node_config, logger) >> diff --git >> a/dts/framework/remote_session/remote/interactive_remote_session.py >> b/dts/framework/remote_session/remote/interactive_remote_session.py >> new file mode 100644 >> index 00000000..2d94daf2 >> --- /dev/null >> +++ b/dts/framework/remote_session/remote/interactive_remote_session.py >> @@ -0,0 +1,82 @@ >> +# SPDX-License-Identifier: BSD-3-Clause >> +# Copyright(c) 2023 University of New Hampshire >> + >> +import socket >> +import traceback >> + >> +from paramiko import AutoAddPolicy, SSHClient, Transport # type: ignore >> +from paramiko.ssh_exception import ( # type: ignore >> + AuthenticationException, >> + BadHostKeyException, >> + NoValidConnectionsError, >> + SSHException, >> +) >> + >> +from framework.config import NodeConfiguration >> +from framework.exception import SSHConnectionError >> +from framework.logger import DTSLOG >> + >> + >> +class InteractiveRemoteSession: >> + hostname: str >> + ip: str >> + port: int >> + username: str >> + password: str >> + _logger: DTSLOG >> + _node_config: NodeConfiguration >> + session: SSHClient >> + _transport: Transport | None >> + >> + def __init__(self, node_config: NodeConfiguration, _logger: DTSLOG) -> >> None: >> + self._node_config = node_config >> + self._logger = _logger >> + self.hostname = node_config.hostname >> + self.username = node_config.user >> + self.password = node_config.password if node_config.password else "" >> + port = "22" >> + self.ip = node_config.hostname >> + if ":" in node_config.hostname: >> + self.ip, port = node_config.hostname.split(":") >> + self.port = int(port) >> + self._logger.info( >> + f"Initializing interactive connection for >> {self.username}@{self.hostname}" >> + ) >> + self._connect() >> + self._logger.info( >> + f"Interactive connection successful for >> {self.username}@{self.hostname}" >> + ) >> + >> + def _connect(self) -> None: >> + client = SSHClient() >> + client.set_missing_host_key_policy(AutoAddPolicy) >> + self.session = client >> + retry_attempts = 10 >> + for retry_attempt in range(retry_attempts): >> + try: >> + client.connect( >> + self.ip, >> + username=self.username, >> + port=self.port, >> + password=self.password, >> + timeout=20 if self.port else 10, >> + ) >> + except (TypeError, BadHostKeyException, >> AuthenticationException) as e: >> + self._logger.exception(e) >> + raise SSHConnectionError(self.hostname) from e >> + except (NoValidConnectionsError, socket.error, SSHException) as >> e: >> + self._logger.debug(traceback.format_exc()) >> + self._logger.warning(e) >> + self._logger.info( >> + "Retrying interactive session connection: " >> + f"retry number {retry_attempt +1}" >> + ) >> + else: >> + break >> + else: >> + raise SSHConnectionError(self.hostname) >> + # Interactive sessions are used on an "as needed" basis so we have >> + # to set a keepalive >> + self._transport = self.session.get_transport() >> + if self._transport is not None: >> + self._transport.set_keepalive(30) >> diff --git a/dts/framework/remote_session/remote/interactive_shell.py >> b/dts/framework/remote_session/remote/interactive_shell.py >> new file mode 100644 >> index 00000000..0cb0f7b4 >> --- /dev/null >> +++ b/dts/framework/remote_session/remote/interactive_shell.py >> @@ -0,0 +1,75 @@ >> +from pathlib import PurePath >> + >> +from paramiko import Channel, SSHClient, channel # type: ignore >> + >> +from framework.logger import DTSLOG >> +from framework.settings import SETTINGS >> + >> + >> +class InteractiveShell: >> + >> + _interactive_session: SSHClient >> + _stdin: channel.ChannelStdinFile >> + _stdout: channel.ChannelFile >> + _ssh_channel: Channel >> + _logger: DTSLOG >> + _timeout: float >> + _path_to_app: PurePath >> + >> + def __init__( >> + self, >> + interactive_session: SSHClient, >> + logger: DTSLOG, >> + path_to_app: PurePath, >> + timeout: float = SETTINGS.timeout, >> + ) -> None: >> + self._interactive_session = interactive_session >> + self._ssh_channel = self._interactive_session.invoke_shell() >> + self._stdin = self._ssh_channel.makefile_stdin("w") >> + self._stdout = self._ssh_channel.makefile("r") >> + self._ssh_channel.settimeout(timeout) >> + self._ssh_channel.set_combine_stderr(True) # combines stdout and >> stderr streams >> + self._logger = logger >> + self._timeout = timeout >> + self._path_to_app = path_to_app >> + self._start_application() >> + >> + def _start_application(self) -> None: >> + """Starts a new interactive application based on _path_to_app. >> + >> + This method is often overridden by subclasses as their process for >> + starting may look different. >> + """ >> + self.send_command_get_output(f"{self._path_to_app}", "") >> + >> + def send_command_get_output(self, command: str, prompt: str) -> str: >> + """Send a command and get all output before the expected ending >> string. >> + >> + Lines that expect input are not included in the stdout buffer so >> they cannot be >> + used for expect. For example, if you were prompted to log into >> something >> + with a username and password, you cannot expect "username:" because >> it won't >> + yet be in the stdout buffer. A work around for this could be >> consuming an >> + extra newline character to force the current prompt into the stdout >> buffer. >> + >> + Returns: >> + All output in the buffer before expected string >> + """ >> + self._logger.info(f"Sending command {command.strip()}...") >> + self._stdin.write(f"{command}\n") >> + self._stdin.flush() >> + out: str = "" >> + for line in self._stdout: >> + out += line >> + if prompt in line and not line.rstrip().endswith( >> + command.rstrip() >> + ): # ignore line that sent command >> + break >> + self._logger.debug(f"Got output: {out}") >> + return out >> + >> + def close(self) -> None: >> + self._stdin.close() >> + self._ssh_channel.close() >> + >> + def __del__(self) -> None: >> + self.close() >> diff --git a/dts/framework/remote_session/remote/testpmd_shell.py >> b/dts/framework/remote_session/remote/testpmd_shell.py >> new file mode 100644 >> index 00000000..31c25258 >> --- /dev/null >> +++ b/dts/framework/remote_session/remote/testpmd_shell.py >> @@ -0,0 +1,75 @@ >> +# SPDX-License-Identifier: BSD-3-Clause >> +# Copyright(c) 2023 University of New Hampshire >> + >> + >> +from pathlib import PurePath >> + >> +from paramiko import SSHClient # type: ignore >> + >> +from framework.logger import DTSLOG >> +from framework.settings import SETTINGS >> + >> +from .interactive_shell import InteractiveShell >> + >> + >> +class TestPmdDevice(object): >> + pci_address: str >> + >> + def __init__(self, pci_address: str): >> + self.pci_address = pci_address >> + >> + def __str__(self) -> str: >> + return self.pci_address >> + >> + >> +class TestPmdShell(InteractiveShell): >> + expected_prompt: str = "testpmd>" >> + _eal_flags: str >> + >> + def __init__( >> + self, >> + interactive_session: SSHClient, >> + logger: DTSLOG, >> + path_to_testpmd: PurePath, >> + eal_flags: str, >> + timeout: float = SETTINGS.timeout, >> + ) -> None: >> + """Initializes an interactive testpmd session using specified >> parameters.""" >> + self._eal_flags = eal_flags >> + >> + super(TestPmdShell, self).__init__( >> + interactive_session, >> + logger=logger, >> + path_to_app=path_to_testpmd, >> + timeout=timeout, >> + ) >> + >> + def _start_application(self) -> None: >> + """Starts a new interactive testpmd shell using _path_to_app.""" >> + self.send_command( >> + f"{self._path_to_app} {self._eal_flags} -- -i", >> + ) >> + >> + def send_command(self, command: str, prompt: str = expected_prompt) -> >> str: >> + """Specific way of handling the command for testpmd >> + >> + An extra newline character is consumed in order to force the >> current line into >> + the stdout buffer. >> + """ >> + return self.send_command_get_output(f"{command}\n", prompt) >> + >> + def get_devices(self) -> list[TestPmdDevice]: >> + """Get a list of device names that are known to testpmd >> + >> + Uses the device info listed in testpmd and then parses the output to >> + return only the names of the devices. >> + >> + Returns: >> + A list of strings representing device names (e.g. 0000:14:00.1) >> + """ >> + dev_info: str = self.send_command("show device info all") >> + dev_list: list[TestPmdDevice] = [] >> + for line in dev_info.split("\n"): >> + if "device name:" in line.lower(): >> + dev_list.append(TestPmdDevice(line.strip().split(": >> ")[1].strip())) >> + return dev_list >> diff --git a/dts/framework/test_result.py b/dts/framework/test_result.py >> index 74391982..fe199467 100644 >> --- a/dts/framework/test_result.py >> +++ b/dts/framework/test_result.py >> @@ -1,5 +1,6 @@ >> # SPDX-License-Identifier: BSD-3-Clause >> # Copyright(c) 2023 PANTHEON.tech s.r.o. >> +# Copyright(c) 2023 University of New Hampshire >> >> """ >> Generic result container and reporters >> @@ -13,9 +14,11 @@ >> OS, >> Architecture, >> BuildTargetConfiguration, >> + BuildTargetInfo, >> Compiler, >> CPUType, >> NodeConfiguration, >> + NodeInfo, >> ) >> from .exception import DTSError, ErrorSeverity >> from .logger import DTSLOG >> @@ -67,12 +70,14 @@ class Statistics(dict): >> Using a dict provides a convenient way to format the data. >> """ >> >> - def __init__(self, dpdk_version): >> + def __init__(self, output_info: dict[str, str] | None): >> super(Statistics, self).__init__() >> for result in Result: >> self[result.name] = 0 >> self["PASS RATE"] = 0.0 >> - self["DPDK VERSION"] = dpdk_version >> + if output_info: >> + for info_key, info_val in output_info.items(): >> + self[info_key] = info_val >> >> def __iadd__(self, other: Result) -> "Statistics": >> """ >> @@ -206,6 +211,8 @@ class BuildTargetResult(BaseResult): >> os: OS >> cpu: CPUType >> compiler: Compiler >> + compiler_version: str | None >> + dpdk_version: str | None >> >> def __init__(self, build_target: BuildTargetConfiguration): >> super(BuildTargetResult, self).__init__() >> @@ -213,6 +220,12 @@ def __init__(self, build_target: >> BuildTargetConfiguration): >> self.os = build_target.os >> self.cpu = build_target.cpu >> self.compiler = build_target.compiler >> + self.compiler_version = None >> + self.dpdk_version = None >> + >> + def add_build_target_versions(self, versions: BuildTargetInfo) -> None: >> + self.compiler_version = versions.compiler_version >> + self.dpdk_version = versions.dpdk_version >> >> def add_test_suite(self, test_suite_name: str) -> TestSuiteResult: >> test_suite_result = TestSuiteResult(test_suite_name) >> @@ -228,10 +241,17 @@ class ExecutionResult(BaseResult): >> """ >> >> sut_node: NodeConfiguration >> + sut_os_name: str >> + sut_os_version: str >> + sut_kernel_version: str >> >> - def __init__(self, sut_node: NodeConfiguration): >> + def __init__(self, sut_node: NodeConfiguration, sut_version_info: >> NodeInfo): >> super(ExecutionResult, self).__init__() >> self.sut_node = sut_node >> + self.sut_version_info = sut_version_info >> + self.sut_os_name = sut_version_info.os_name >> + self.sut_os_version = sut_version_info.os_version >> + self.sut_kernel_version = sut_version_info.kernel_version >> >> def add_build_target( >> self, build_target: BuildTargetConfiguration >> @@ -258,6 +278,7 @@ class DTSResult(BaseResult): >> """ >> >> dpdk_version: str | None >> + output: dict | None >> _logger: DTSLOG >> _errors: list[Exception] >> _return_code: ErrorSeverity >> @@ -267,14 +288,17 @@ class DTSResult(BaseResult): >> def __init__(self, logger: DTSLOG): >> super(DTSResult, self).__init__() >> self.dpdk_version = None >> + self.output = None >> self._logger = logger >> self._errors = [] >> self._return_code = ErrorSeverity.NO_ERR >> self._stats_result = None >> self._stats_filename = os.path.join(SETTINGS.output_dir, >> "statistics.txt") >> >> - def add_execution(self, sut_node: NodeConfiguration) -> ExecutionResult: >> - execution_result = ExecutionResult(sut_node) >> + def add_execution( >> + self, sut_node: NodeConfiguration, sut_version_info: NodeInfo >> + ) -> ExecutionResult: >> + execution_result = ExecutionResult(sut_node, sut_version_info) >> self._inner_results.append(execution_result) >> return execution_result >> >> @@ -296,7 +320,8 @@ def process(self) -> None: >> for error in self._errors: >> self._logger.debug(repr(error)) >> >> - self._stats_result = Statistics(self.dpdk_version) >> + self._stats_result = Statistics(self.output) >> + # add information gathered from the smoke tests to the statistics >> self.add_stats(self._stats_result) >> with open(self._stats_filename, "w+") as stats_file: >> stats_file.write(str(self._stats_result)) >> diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py >> index 0705f38f..de94c933 100644 >> --- a/dts/framework/test_suite.py >> +++ b/dts/framework/test_suite.py >> @@ -11,7 +11,12 @@ >> import re >> from types import MethodType >> >> -from .exception import ConfigurationError, SSHTimeoutError, >> TestCaseVerifyError >> +from .exception import ( >> + BlockingTestSuiteError, >> + ConfigurationError, >> + SSHTimeoutError, >> + TestCaseVerifyError, >> +) >> from .logger import DTSLOG, getLogger >> from .settings import SETTINGS >> from .test_result import BuildTargetResult, Result, TestCaseResult, >> TestSuiteResult >> @@ -37,6 +42,7 @@ class TestSuite(object): >> """ >> >> sut_node: SutNode >> + is_blocking = False >> _logger: DTSLOG >> _test_cases_to_run: list[str] >> _func: bool >> @@ -118,6 +124,8 @@ def run(self) -> None: >> f"the next test suite may be affected." >> ) >> self._result.update_setup(Result.ERROR, e) >> + if len(self._result.get_errors()) > 0 and self.is_blocking: >> + raise BlockingTestSuiteError(test_suite_name) >> >> def _execute_test_suite(self) -> None: >> """ >> diff --git a/dts/framework/testbed_model/node.py >> b/dts/framework/testbed_model/node.py >> index d48fafe6..c5147e0e 100644 >> --- a/dts/framework/testbed_model/node.py >> +++ b/dts/framework/testbed_model/node.py >> @@ -40,6 +40,7 @@ class Node(object): >> lcores: list[LogicalCore] >> _logger: DTSLOG >> _other_sessions: list[OSSession] >> + _execution_config: ExecutionConfiguration >> >> def __init__(self, node_config: NodeConfiguration): >> self.config = node_config >> @@ -64,6 +65,7 @@ def set_up_execution(self, execution_config: >> ExecutionConfiguration) -> None: >> """ >> self._setup_hugepages() >> self._set_up_execution(execution_config) >> + self._execution_config = execution_config >> >> def _set_up_execution(self, execution_config: ExecutionConfiguration) >> -> None: >> """ >> diff --git a/dts/framework/testbed_model/sut_node.py >> b/dts/framework/testbed_model/sut_node.py >> index 2b2b50d9..6783e0b8 100644 >> --- a/dts/framework/testbed_model/sut_node.py >> +++ b/dts/framework/testbed_model/sut_node.py >> @@ -1,14 +1,27 @@ >> # SPDX-License-Identifier: BSD-3-Clause >> # Copyright(c) 2010-2014 Intel Corporation >> # Copyright(c) 2023 PANTHEON.tech s.r.o. >> +# Copyright(c) 2023 University of New Hampshire >> >> import os >> import tarfile >> import time >> from pathlib import PurePath >> - >> -from framework.config import BuildTargetConfiguration, NodeConfiguration >> -from framework.remote_session import CommandResult, OSSession >> +from typing import Union >> + >> +from framework.config import ( >> + BuildTargetConfiguration, >> + BuildTargetInfo, >> + InteractiveApp, >> + NodeConfiguration, >> + NodeInfo, >> +) >> +from framework.remote_session import ( >> + CommandResult, >> + InteractiveShell, >> + OSSession, >> + TestPmdShell, >> +) >> from framework.settings import SETTINGS >> from framework.utils import EnvVarsDict, MesonArgs >> >> @@ -16,6 +29,52 @@ >> from .node import Node >> >> >> +class EalParameters(object): >> + def __init__( >> + self, >> + lcore_list: LogicalCoreList, >> + memory_channels: int, >> + prefix: str, >> + no_pci: bool, >> + vdevs: list[VirtualDevice], >> + other_eal_param: str, >> + ): >> + """ >> + Generate eal parameters character string; >> + :param lcore_list: the list of logical cores to use. >> + :param memory_channels: the number of memory channels to use. >> + :param prefix: set file prefix string, eg: >> + prefix='vf' >> + :param no_pci: switch of disable PCI bus eg: >> + no_pci=True >> + :param vdevs: virtual device list, eg: >> + vdevs=[ >> + VirtualDevice('net_ring0'), >> + VirtualDevice('net_ring1') >> + ] >> + :param other_eal_param: user defined DPDK eal parameters, eg: >> + other_eal_param='--single-file-segments' >> + """ >> + self._lcore_list = f"-l {lcore_list}" >> + self._memory_channels = f"-n {memory_channels}" >> + self._prefix = prefix >> + if prefix: >> + self._prefix = f"--file-prefix={prefix}" >> + self._no_pci = "--no-pci" if no_pci else "" >> + self._vdevs = " ".join(f"--vdev {vdev}" for vdev in vdevs) >> + self._other_eal_param = other_eal_param >> + >> + def __str__(self) -> str: >> + return ( >> + f"{self._lcore_list} " >> + f"{self._memory_channels} " >> + f"{self._prefix} " >> + f"{self._no_pci} " >> + f"{self._vdevs} " >> + f"{self._other_eal_param}" >> + ) >> + >> + >> class SutNode(Node): >> """ >> A class for managing connections to the System under Test, providing >> @@ -30,9 +89,11 @@ class SutNode(Node): >> _env_vars: EnvVarsDict >> _remote_tmp_dir: PurePath >> __remote_dpdk_dir: PurePath | None >> - _dpdk_version: str | None >> _app_compile_timeout: float >> _dpdk_kill_session: OSSession | None >> + _dpdk_version: str | None >> + _node_info: NodeInfo | None >> + _compiler_version: str | None >> >> def __init__(self, node_config: NodeConfiguration): >> super(SutNode, self).__init__(node_config) >> @@ -41,12 +102,14 @@ def __init__(self, node_config: NodeConfiguration): >> self._env_vars = EnvVarsDict() >> self._remote_tmp_dir = self.main_session.get_remote_tmp_dir() >> self.__remote_dpdk_dir = None >> - self._dpdk_version = None >> self._app_compile_timeout = 90 >> self._dpdk_kill_session = None >> self._dpdk_timestamp = ( >> f"{str(os.getpid())}_{time.strftime('%Y%m%d%H%M%S', >> time.localtime())}" >> ) >> + self._dpdk_version = None >> + self._node_info = None >> + self._compiler_version = None >> >> @property >> def _remote_dpdk_dir(self) -> PurePath: >> @@ -75,6 +138,32 @@ def dpdk_version(self) -> str: >> ) >> return self._dpdk_version >> >> + @property >> + def node_info(self) -> NodeInfo: >> + if self._node_info is None: >> + self._node_info = self.main_session.get_node_info() >> + return self._node_info >> + >> + @property >> + def compiler_version(self) -> str: >> + if self._compiler_version is None: >> + if self._build_target_config is not None: >> + self._compiler_version = >> self.main_session.get_compiler_version( >> + self._build_target_config.compiler.name >> + ) >> + else: >> + self._logger.warning( >> + "Failed to get compiler version because" >> + "_build_target_config is None." >> + ) >> + return "" >> + return self._compiler_version >> + >> + def get_build_target_info(self) -> BuildTargetInfo: >> + return BuildTargetInfo( >> + dpdk_version=self.dpdk_version, >> compiler_version=self.compiler_version >> + ) >> + >> def _guess_dpdk_remote_dir(self) -> PurePath: >> return self.main_session.guess_dpdk_remote_dir(self._remote_tmp_dir) >> >> @@ -84,6 +173,10 @@ def _set_up_build_target( >> """ >> Setup DPDK on the SUT node. >> """ >> + # we want to ensure that dpdk_version and compiler_version is reset >> for new >> + # build targets >> + self._dpdk_version = None >> + self._compiler_version = None >> self._configure_build_target(build_target_config) >> self._copy_dpdk_tarball() >> self._build_dpdk() >> @@ -262,48 +355,37 @@ def run_dpdk_app( >> f"{app_path} {eal_args}", timeout, verify=True >> ) >> >> - >> -class EalParameters(object): >> - def __init__( >> + def create_interactive_shell( >> self, >> - lcore_list: LogicalCoreList, >> - memory_channels: int, >> - prefix: str, >> - no_pci: bool, >> - vdevs: list[VirtualDevice], >> - other_eal_param: str, >> - ): >> + shell_type: InteractiveApp, >> + timeout: float = SETTINGS.timeout, >> + eal_parameters: EalParameters | None = None, >> + ) -> Union[InteractiveShell, TestPmdShell]: >> + """Create a handler for an interactive session. >> + >> + This method is a factory that calls a method in OSSession to create >> shells for >> + different DPDK applications. >> + >> + Args: >> + shell_type: Enum value representing the desired application. >> + timeout: Timeout for reading output from the SSH channel. If >> you are >> + reading from the buffer and don't receive any data within >> the timeout >> + it will throw an error. >> + eal_parameters: List of EAL parameters to use to launch the >> app. If this >> + isn't provided, it will default to calling >> create_eal_parameters(). >> + This is ignored for base "shell" types. >> + Returns: >> + Instance of the desired interactive application. >> """ >> - Generate eal parameters character string; >> - :param lcore_list: the list of logical cores to use. >> - :param memory_channels: the number of memory channels to use. >> - :param prefix: set file prefix string, eg: >> - prefix='vf' >> - :param no_pci: switch of disable PCI bus eg: >> - no_pci=True >> - :param vdevs: virtual device list, eg: >> - vdevs=[ >> - VirtualDevice('net_ring0'), >> - VirtualDevice('net_ring1') >> - ] >> - :param other_eal_param: user defined DPDK eal parameters, eg: >> - other_eal_param='--single-file-segments' >> - """ >> - self._lcore_list = f"-l {lcore_list}" >> - self._memory_channels = f"-n {memory_channels}" >> - self._prefix = prefix >> - if prefix: >> - self._prefix = f"--file-prefix={prefix}" >> - self._no_pci = "--no-pci" if no_pci else "" >> - self._vdevs = " ".join(f"--vdev {vdev}" for vdev in vdevs) >> - self._other_eal_param = other_eal_param >> - >> - def __str__(self) -> str: >> - return ( >> - f"{self._lcore_list} " >> - f"{self._memory_channels} " >> - f"{self._prefix} " >> - f"{self._no_pci} " >> - f"{self._vdevs} " >> - f"{self._other_eal_param}" >> + if not eal_parameters: >> + eal_parameters = self.create_eal_parameters() >> + >> + # We need to append the build directory for DPDK apps >> + shell_type.path = >> self.remote_dpdk_build_dir.joinpath(shell_type.path) >> + default_path = self.main_session.join_remote_path(shell_type.path) >> + return self.main_session.create_interactive_shell( >> + shell_type, >> + default_path, >> + str(eal_parameters), >> + timeout, >> ) >> diff --git a/dts/framework/utils.py b/dts/framework/utils.py >> index 55e0b0ef..2ee67145 100644 >> --- a/dts/framework/utils.py >> +++ b/dts/framework/utils.py >> @@ -5,6 +5,8 @@ >> >> import sys >> >> +REGEX_FOR_PCI_ADDRESS = >> "/[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}.[0-9]{1}/" >> + >> >> def check_dts_python_version() -> None: >> if sys.version_info.major < 3 or ( >> diff --git a/dts/tests/TestSuite_smoke_tests.py >> b/dts/tests/TestSuite_smoke_tests.py >> new file mode 100644 >> index 00000000..9cf54720 >> --- /dev/null >> +++ b/dts/tests/TestSuite_smoke_tests.py >> @@ -0,0 +1,113 @@ >> +# SPDX-License-Identifier: BSD-3-Clause >> +# Copyright(c) 2023 University of New Hampshire >> + >> +import re >> + >> +from framework.config import InteractiveApp, PortConfig >> +from framework.remote_session import TestPmdDevice, TestPmdShell >> +from framework.settings import SETTINGS >> +from framework.test_suite import TestSuite >> +from framework.utils import REGEX_FOR_PCI_ADDRESS >> + >> + >> +class SmokeTests(TestSuite): >> + is_blocking = True >> + # dicts in this list are expected to have two keys: >> + # "pci_address" and "current_driver" >> + nics_in_node: list[PortConfig] = [] >> + >> + def set_up_suite(self) -> None: >> + """ >> + Setup: >> + Set the build directory path and generate a list of NICs in the >> SUT node. >> + """ >> + self.dpdk_build_dir_path = self.sut_node.remote_dpdk_build_dir >> + self.nics_in_node = self.sut_node.config.ports >> + >> + def test_unit_tests(self) -> None: >> + """ >> + Test: >> + Run the fast-test unit-test suite through meson. >> + """ >> + self.sut_node.main_session.send_command( >> + f"meson test -C {self.dpdk_build_dir_path} --suite fast-tests", >> + 300, >> + verify=True, >> + ) >> + >> + def test_driver_tests(self) -> None: >> + """ >> + Test: >> + Run the driver-test unit-test suite through meson. >> + """ >> + list_of_vdevs = "" >> + for dev in self.sut_node._execution_config.vdevs: >> + list_of_vdevs += f"--vdev {dev} " >> + list_of_vdevs = list_of_vdevs[:-1] >> + if list_of_vdevs: >> + self._logger.info( >> + "Running driver tests with the following virtual " >> + f"devices: {list_of_vdevs}" >> + ) >> + self.sut_node.main_session.send_command( >> + f"meson test -C {self.dpdk_build_dir_path} --suite >> driver-tests " >> + f'--test-args "{list_of_vdevs}"', >> + 300, >> + verify=True, >> + ) >> + else: >> + self.sut_node.main_session.send_command( >> + f"meson test -C {self.dpdk_build_dir_path} --suite >> driver-tests", >> + 300, >> + verify=True, >> + ) >> + >> + def test_devices_listed_in_testpmd(self) -> None: >> + """ >> + Test: >> + Uses testpmd driver to verify that devices have been found by >> testpmd. >> + """ >> + testpmd_driver = >> self.sut_node.create_interactive_shell(InteractiveApp.testpmd) >> + # We know it should always be a TestPmdShell but mypy doesn't >> + assert isinstance(testpmd_driver, TestPmdShell) >> + dev_list: list[TestPmdDevice] = testpmd_driver.get_devices() >> + for nic in self.nics_in_node: >> + self.verify( >> + nic.pci in map(str, dev_list), >> + f"Device {nic.pci} was not listed in testpmd's available >> devices, " >> + "please check your configuration", >> + ) >> + >> + def test_device_bound_to_driver(self) -> None: >> + """ >> + Test: >> + Ensure that all drivers listed in the config are bound to the >> correct driver. >> + """ >> + path_to_devbind = self.sut_node.main_session.join_remote_path( >> + self.sut_node._remote_dpdk_dir, "usertools", "dpdk-devbind.py" >> + ) >> + >> + all_nics_in_dpdk_devbind = self.sut_node.main_session.send_command( >> + f"{path_to_devbind} --status | awk '{REGEX_FOR_PCI_ADDRESS}'", >> + SETTINGS.timeout, >> + ).stdout >> + >> + for nic in self.nics_in_node: >> + # This regular expression finds the line in the above string >> that starts >> + # with the address for the nic we are on in the loop and then >> captures the >> + # name of the driver in a group >> + devbind_info_for_nic = re.search( >> + f"{nic.pci}[^\\n]*drv=([\\d\\w]*) [^\\n]*", >> + all_nics_in_dpdk_devbind, >> + ) >> + self.verify( >> + devbind_info_for_nic is not None, >> + f"Failed to find configured device ({nic.pci}) using >> dpdk-devbind.py", >> + ) >> + # We know this isn't None, but mypy doesn't >> + assert devbind_info_for_nic is not None >> + self.verify( >> + devbind_info_for_nic.group(1) == nic.os_driver, >> + f"Driver for device {nic.pci} does not match driver listed >> in " >> + f"configuration (bound to {devbind_info_for_nic.group(1)})", >> + ) >> -- >> 2.41.0 >>