From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124]) by inbox.dpdk.org (Postfix) with ESMTP id 16B6642E49; Tue, 11 Jul 2023 20:30:17 +0200 (CEST) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id E48E840A7D; Tue, 11 Jul 2023 20:30:16 +0200 (CEST) Received: from mail-pl1-f182.google.com (mail-pl1-f182.google.com [209.85.214.182]) by mails.dpdk.org (Postfix) with ESMTP id 812094003C for ; Tue, 11 Jul 2023 20:30:14 +0200 (CEST) Received: by mail-pl1-f182.google.com with SMTP id d9443c01a7336-1b8b2b60731so27647385ad.2 for ; Tue, 11 Jul 2023 11:30:14 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=iol.unh.edu; s=unh-iol; t=1689100213; x=1691692213; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:from:to:cc:subject:date:message-id:reply-to; bh=431meztYiXLK8DoSXG73QJ5Ar6EBi8kId/6ev0Esm1A=; b=iIQeyll1ovjlzJFWZLePpt6I/8xzCX3rwwMrNhUvs2hUTEnWhchkBt8UDnv7qfQPyQ kVeZZ4T6uCTI41ouBzJFTmZ3rl/gEsLVrjbJwqYQ/rphYXOYBIuFveKbZ36lkZAt26Yy Y4KX7+ZRa8TIjannsxl64IZgx8h45Efu29NBs= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20221208; t=1689100213; x=1691692213; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=431meztYiXLK8DoSXG73QJ5Ar6EBi8kId/6ev0Esm1A=; b=HZv+RAAzuzKsANf9apIepZetOxak3dDDDEnUP8uNXc0zeup2TCC3e4G7XxF9fa4LId TaM3rMzJpSgKDkQk2P+jCfzzQVY5gCJge3YsfW+zUhzdaKhNurtkACbjd41LX8UIGhMe 3wF2D9WayEMk0tZKGlo5Pad4ghOVTcuh8ya9iEhYWji5jdF3f0tiR1KlnxwzPiiRZM00 inQ/CPPGaJxI4qCcxmPSJ3ySxZ7voSsicMyN9WPaCLm3/x3UhnqSSe/lke8oFz/d/GDJ zNulsfo3JR5H0FK5uyrTXSwzcE+vYYQiELS2sRmrykJHm0ONl0F6fTv086c0rSg4idvs Hs+Q== X-Gm-Message-State: ABy/qLYavL3nthFfZrihT6I4uu+h+0iiMaLrcNNZEwKJIoEDKq9XI4Fa Vxug9S2qqK1EnCtmjng0OtFJGlE5fXwql2pYr+J08g== X-Google-Smtp-Source: APBJJlE0sf7Z/GHdqtdg/AaFgywi6Ih6aPQsqcpItFOraayNgbzehmHE4V0WN3643TYS3wbInhjP/sKppRnoHTjKchw= X-Received: by 2002:a17:902:b691:b0:1b8:c1f:fd2c with SMTP id c17-20020a170902b69100b001b80c1ffd2cmr11715039pls.33.1689100213222; Tue, 11 Jul 2023 11:30:13 -0700 (PDT) MIME-Version: 1.0 References: <20230710162104.24425-4-jspewock@iol.unh.edu> <20230710162104.24425-6-jspewock@iol.unh.edu> In-Reply-To: From: Jeremy Spewock Date: Tue, 11 Jul 2023 14:30:01 -0400 Message-ID: Subject: Re: [PATCH v2 1/2] dts: add smoke tests To: =?UTF-8?Q?Juraj_Linke=C5=A1?= Cc: Honnappa.Nagarahalli@arm.com, thomas@monjalon.net, lijuan.tu@intel.com, wathsala.vithanage@arm.com, probb@iol.unh.edu, dev@dpdk.org Content-Type: multipart/alternative; boundary="0000000000003319cb06003a4a96" X-BeenThere: dev@dpdk.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: DPDK patches and discussions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dev-bounces@dpdk.org --0000000000003319cb06003a4a96 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable On Tue, Jul 11, 2023 at 5:42=E2=80=AFAM Juraj Linke=C5=A1 wrote: > Just a few more comments. > > On Mon, Jul 10, 2023 at 6:23=E2=80=AFPM wrote: > > > > From: Jeremy Spewock > > > > 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 > > --- > > dts/conf.yaml | 17 +- > > dts/framework/config/__init__.py | 116 +++++++++-- > > dts/framework/config/conf_yaml_schema.json | 142 +++++++++++++- > > dts/framework/dts.py | 88 ++++++--- > > dts/framework/exception.py | 12 ++ > > dts/framework/remote_session/__init__.py | 10 +- > > dts/framework/remote_session/os_session.py | 24 ++- > > dts/framework/remote_session/posix_session.py | 29 ++- > > .../remote_session/remote/__init__.py | 10 + > > .../remote/interactive_remote_session.py | 118 ++++++++++++ > > .../remote/interactive_shell.py | 99 ++++++++++ > > .../remote_session/remote/testpmd_shell.py | 67 +++++++ > > dts/framework/test_result.py | 37 +++- > > dts/framework/test_suite.py | 21 +- > > dts/framework/testbed_model/node.py | 2 + > > dts/framework/testbed_model/sut_node.py | 180 +++++++++++++----- > > dts/tests/TestSuite_smoke_tests.py | 118 ++++++++++++ > > 17 files changed, 994 insertions(+), 96 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.p= y > > create mode 100644 dts/tests/TestSuite_smoke_tests.py > > > > diff --git a/dts/conf.yaml b/dts/conf.yaml > > index a9bd8a3e..2717de13 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 ski smoke > tests > > Typo: ski > Also put a space after # > > > 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 executio= n > > Missing space after # > The sentence after hugepages has a comma in it, let's unify those. > > > + - "crypto_openssl" > > nodes: > > - name: "SUT 1" > > hostname: sut1.change.me.localhost > > @@ -20,6 +24,17 @@ nodes: > > arch: x86_64 > > os: linux > > lcores: "" > > + ports: > > I'm comparing my version with this patch and I've just noticed this - > let's put the ports at the end (after hugepages). This way we'll have > the configuration sorted into sections of sorts: > Cores/cpu config > Memory config > Port/devices config > > > + - pci: "0000:00:08.0" > > + os_driver_for_dpdk: vfio-pci #OS driver that DPDK will use > > Missing space after # > > Sorry, I'm just used to writing comments without the space but I'll fix this. > > + 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" > > use_first_core: false > > memory_channels: 4 > > hugepages: # optional; if removed, will use system hugepage > configuration > > diff --git a/dts/framework/config/__init__.py > b/dts/framework/config/__init__.py > > index ebb0823f..75ac1cbe 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=3DTrue, frozen=3DTrue) > > +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=3Dnode, **d) > > + > > + > > @dataclass(slots=3DTrue, frozen=3DTrue) > > 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,18 +108,43 @@ def from_dict(d: dict) -> "NodeConfiguration": > > if "force_first_numa" not in hugepage_config: > > hugepage_config["force_first_numa"] =3D False > > hugepage_config =3D HugepageConfiguration(**hugepage_confi= g) > > + common_config =3D { > > + "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 i= n > d["ports"]], > > + } > > + > > + return NodeConfiguration(**common_config) > > + > > + > > +@dataclass(slots=3DTrue) > > Looks like this could be frozen as well. This should work even in the > future, as I imagine we'll get all the info we need just once. > You're right, this data isn't really something that's going to change once we collect it and it's probably better to enforce that. I'll make this change. > > +class NodeInfo: > > + """Class to hold important versions within the node. > > + > > + This class, unlike the NodeConfiguration class, cannot be generate= d > 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. > > + """ > > > > - return NodeConfiguration( > > - name=3Dd["name"], > > - hostname=3Dd["hostname"], > > - user=3Dd["user"], > > - password=3Dd.get("password"), > > - arch=3DArchitecture(d["arch"]), > > - os=3DOS(d["os"]), > > - lcores=3Dd.get("lcores", "1"), > > - use_first_core=3Dd.get("use_first_core", False), > > - memory_channels=3Dd.get("memory_channels", 1), > > - hugepages=3Dhugepage_config, > > + os_name: str > > + os_version: str > > + kernel_version: str > > + > > + @staticmethod > > + def from_dict(d: dict): > > + return NodeInfo( > > + os_name=3Dd["os_name"], > > + os_version=3Dd["os_version"], > > + kernel_version=3Dd["kernel_version"], > > ) > > We don't need the from_dict method, as we can instantiate this class > right away (node_info =3D NodeInfo(os_name=3Dd["os_name"], > os_version=3Dd["os_version"], kernel_version=3Dd["kernel_version"])). > > The other classes need this method because we're doing some processing > before instantiating the classes - this one doesn't need it. > Good point, I included it to make it similar to the others but it really isn't necessary because I'm not even using it. > > > > > > > @@ -128,6 +169,24 @@ def from_dict(d: dict) -> > "BuildTargetConfiguration": > > ) > > > > > > +@dataclass(slots=3DTrue) > > Can also be frozen. > > > +class BuildTargetInfo: > > + """Class to hold important versions within the build target. > > + > > + This is very similar to the NodeVersionInfo class, it just instead > holds information > > References renamed class. > Good catch, I totally missed the reference to the other class in this comment. > > > + for the build target. > > + """ > > + > > + dpdk_version: str > > + compiler_version: str > > + > > + @staticmethod > > + def from_dict(d: dict): > > + return BuildTargetInfo( > > + dpdk_version=3Dd["dpdk_version"], > compiler_version=3Dd["compiler_version"] > > + ) > > Same as above. > Good catches on these as well, I'll be sure to make the changes. > > > + > > + > > class TestSuiteConfigDict(TypedDict): > > suite: str > > cases: list[str] > > @@ -157,6 +216,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 +227,20 @@ def from_dict(d: dict, node_map: dict) -> > "ExecutionConfiguration": > > test_suites: list[TestSuiteConfig] =3D list( > > map(TestSuiteConfig.from_dict, d["test_suites"]) > > ) > > - sut_name =3D d["system_under_test"] > > + sut_name =3D d["system_under_test"]["node_name"] > > + skip_smoke_tests =3D d.get("skip_smoke_tests", False) > > assert sut_name in node_map, f"Unknown SUT {sut_name} in > execution {d}" > > - > > + vdevs =3D ( > > + d["system_under_test"]["vdevs"] if "vdevs" in > d["system_under_test"] else [] > > + ) > > return ExecutionConfiguration( > > build_targets=3Dbuild_targets, > > perf=3Dd["perf"], > > func=3Dd["func"], > > + skip_smoke_tests=3Dskip_smoke_tests, > > test_suites=3Dtest_suites, > > system_under_test=3Dnode_map[sut_name], > > + vdevs=3Dvdevs, > > ) > > > > > > @@ -221,3 +287,27 @@ def load_config() -> Configuration: > > > > > > CONFIGURATION =3D 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 PurPath object fo= r > 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. For ever= y > key other > > + than the default shell option, the path will be appended to the > path to the DPDK > > + build directory for the current SUT node. > > + """ > > + > > + shell =3D {"default_path": PurePath()} > > + testpmd =3D {"default_path": PurePath("app", "dpdk-testpmd")} > > + > > + def get_path(self) -> PurePath: > > + """A method for getting the default paths of an application > > + > > + Returns: > > + String array that represents an OS agnostic path to the > application. > > + """ > > + return self.value["default_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 b= ut > greatly simplifies implementation. If there are an inconsistencies, then > DTS will not run until that issue is fixed. An example inconsistency woul= d > be port 1, node 1 says it is connected to port 1, node 2, but port 1, nod= e > 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 por= t > (ex: i40e)" > > + }, > > + "peer_node": { > > + "type": "string", > > + "description": "The name of the node the peer port i= s > 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 b= e > 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..1b67938f 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 =3D result.add_execution(sut_node.config) > > + execution_result =3D 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 =3D sut_node.dpdk_version > > + # result.dpdk_version =3D sut_node.dpdk_version > > + > build_target_result.add_build_target_versions(sut_node.get_build_target_i= nfo()) > > 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,62 @@ def _run_suites( > > with possibly only a subset of test cases. > > If no subset is specified, run all test cases. > > """ > > + end_build_target =3D False > > + if not execution.skip_smoke_tests: > > + execution.test_suites[:0] =3D > [TestSuiteConfig.from_dict("smoke_tests")] > > for test_suite_config in execution.test_suites: > > try: > > - full_suite_path =3D > f"tests.TestSuite_{test_suite_config.test_suite}" > > - test_suite_classes =3D get_test_suites(full_suite_path) > > - suites_str =3D ", ".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 =3D 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 =3D 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 =3D > f"tests.TestSuite_{test_suite_config.test_suite}" > > + test_suite_classes =3D get_test_suites(full_suite_path) > > + suites_str =3D ", ".join((x.__name__ for x in test_suite_class= es)) > > + 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 tes= t > suites.") > > + result.update_setup(Result.ERROR, e) > > + > > + else: > > + for test_suite_class in test_suite_classes: > > + test_suite =3D test_suite_class( > > + sut_node, > > + test_suite_config.test_cases, > > + execution.func, > > + build_target_result, > > + 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 =3D 4 > > DPDK_BUILD_ERR =3D 10 > > TESTCASE_VERIFY_ERR =3D 20 > > + BLOCKING_TESTSUITE_ERR =3D 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] =3D > ErrorSeverity.BLOCKING_TESTSUITE_ERR > > + > > + def __init__(self, suite_name: str) -> None: > > + self.suite_name =3D 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..4fe32d35 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,14 @@ > > > > from .linux_session import LinuxSession > > from .os_session import OSSession > > -from .remote import CommandResult, RemoteSession, SSHSession > > +from .remote import ( > > + CommandResult, > > + InteractiveRemoteSession, > > + InteractiveShell, > > + RemoteSession, > > + SSHSession, > > + TestPmdShell, > > +) > > > > > > def create_session( > > diff --git a/dts/framework/remote_session/os_session.py > b/dts/framework/remote_session/os_session.py > > index 4c48ae25..4346ecc4 100644 > > --- a/dts/framework/remote_session/os_session.py > > +++ b/dts/framework/remote_session/os_session.py > > @@ -6,13 +6,19 @@ > > from collections.abc import Iterable > > from pathlib import PurePath > > > > -from framework.config import Architecture, NodeConfiguration > > +from framework.config import Architecture, NodeConfiguration, NodeInfo > > from framework.logger import DTSLOG > > from framework.settings import SETTINGS > > from framework.testbed_model import LogicalCore > > from framework.utils import EnvVarsDict, MesonArgs > > > > -from .remote import CommandResult, RemoteSession, create_remote_sessio= n > > +from .remote import ( > > + CommandResult, > > + InteractiveRemoteSession, > > + RemoteSession, > > + create_interactive_session, > > + create_remote_session, > > +) > > > > > > class OSSession(ABC): > > @@ -26,6 +32,7 @@ class OSSession(ABC): > > name: str > > _logger: DTSLOG > > remote_session: RemoteSession > > + interactive_session: InteractiveRemoteSession > > > > def __init__( > > self, > > @@ -37,6 +44,7 @@ def __init__( > > self.name =3D name > > self._logger =3D logger > > self.remote_session =3D create_remote_session(node_config, nam= e, > logger) > > + self.interactive_session =3D > create_interactive_session(node_config, name, logger) > > > > def close(self, force: bool =3D False) -> None: > > """ > > @@ -173,3 +181,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..f8ec159f 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", > 60).stdout.split( > > + "\n" > > + )[0] > > The timeouts are still there. > > > + case "clang": > > + return self.send_command(f"{compiler_name} --version", > 60).stdout.split( > > + "\n" > > + )[0] > > + case "msvc": > > + return self.send_command("cl", 60).stdout > > + case "icc": > > + return self.send_command(f"{compiler_name} -V", > 60).stdout > > + case _: > > + raise ValueError(f"Unknown compiler {compiler_name}") > > + > > + def get_node_info(self) -> NodeInfo: > > + os_release_info =3D self.send_command( > > + "awk -F=3D '$1 ~ /^NAME$|^VERSION$/ {print $2}' > /etc/os-release", > > + SETTINGS.timeout, > > + ).stdout.split("\n") > > + kernel_version =3D 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..224598a8 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) 2022-2023 University of New Hampshire > > There are other instances of the copyright statement. It's not > necessary to change them all to just 2023, but I'd say it's > preferable. > I agree. Knowing more about them after your other comments, this should also be changed. > > > > > # pylama:ignore=3DW0611 > > > > 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 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..e145d35d > > --- /dev/null > > +++ b/dts/framework/remote_session/remote/interactive_remote_session.py > > @@ -0,0 +1,118 @@ > > +# SPDX-License-Identifier: BSD-3-Clause > > +# Copyright(c) 2023 University of New Hampshire > > + > > +import socket > > +import traceback > > +from pathlib import PurePath > > +from typing import Union > > + > > +from paramiko import AutoAddPolicy, SSHClient, Transport # type: igno= re > > +from paramiko.ssh_exception import ( # type: ignore > > + AuthenticationException, > > + BadHostKeyException, > > + NoValidConnectionsError, > > + SSHException, > > +) > > + > > +from framework.config import InteractiveApp, NodeConfiguration > > +from framework.exception import SSHConnectionError > > +from framework.logger import DTSLOG > > + > > +from .interactive_shell import InteractiveShell > > +from .testpmd_shell import TestPmdShell > > + > > + > > +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 =3D node_config > > + self._logger =3D _logger > > + self.hostname =3D node_config.hostname > > + self.username =3D node_config.user > > + self.password =3D node_config.password if node_config.password > else "" > > + port =3D "22" > > + self.ip =3D node_config.hostname > > + if ":" in node_config.hostname: > > + self.ip, port =3D node_config.hostname.split(":") > > + self.port =3D 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 =3D SSHClient() > > + client.set_missing_host_key_policy(AutoAddPolicy) > > + self.session =3D client > > + retry_attempts =3D 10 > > + for retry_attempt in range(retry_attempts): > > + try: > > + client.connect( > > + self.ip, > > + username=3Dself.username, > > + port=3Dself.port, > > + password=3Dself.password, > > + timeout=3D20 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 =3D self.session.get_transport() > > + if self._transport is not None: > > + self._transport.set_keepalive(30) > > + > > + 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.shell: > > + return InteractiveShell( > > + self.session, self._logger, path_to_app, timeout > > + ) > > + case InteractiveApp.testpmd: > > + return TestPmdShell( > > + self.session, > > + self._logger, > > + path_to_app, > > + timeout=3Dtimeout, > > + eal_flags=3Deal_parameters, > > + ) > > + case _: > > + self._logger.info( > > + f"Unhandled app type {shell_type.name}, defaulting > to shell." > > + ) > > + return InteractiveShell( > > + self.session, self._logger, path_to_app, timeout > > + ) > > 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..4b0735c8 > > --- /dev/null > > +++ b/dts/framework/remote_session/remote/interactive_shell.py > > @@ -0,0 +1,99 @@ > > +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 =3D SETTINGS.timeout, > > + ) -> None: > > + self._interactive_session =3D interactive_session > > + self._ssh_channel =3D self._interactive_session.invoke_shell() > > + self._stdin =3D self._ssh_channel.makefile_stdin("w") > > + self._stdout =3D self._ssh_channel.makefile("r") > > + self._ssh_channel.settimeout(timeout) > > + self._ssh_channel.set_combine_stderr(True) # combines stdout > and stderr streams > > + self._logger =3D logger > > + self._timeout =3D timeout > > + self._path_to_app =3D path_to_app > > + > > + > > + def send_command_no_output(self, command: str) -> None: > > + """Send command to channel without recording output. > > + > > + This method will not verify any input or output, it will simpl= y > assume the > > + command succeeded. This method will also consume all output in > the buffer > > + after executing the command. > > + """ > > + self._logger.info( > > + f"Sending command {command.strip()} and not collecting > output" > > + ) > > + self._stdin.write(f"{command}\n") > > + self._stdin.flush() > > + self.empty_stdout_buffer() > > + > > + def empty_stdout_buffer(self) -> None: > > + """Removes all data from the stdout buffer. > > + > > + Because of the way paramiko handles read buffers, there is no > way to effectively > > + remove data from, or "flush", read buffers. This method > essentially moves our > > + offset on the buffer to the end and thus "removes" the data > from the buffer. > > + Timeouts are thrown on read operations of paramiko pipes based > on whether data > > + had been received before timeout so we assume that if we reach > the timeout then > > + we are at the end of the buffer. > > + """ > > + self._ssh_channel.settimeout(0.5) > > + try: > > + for line in self._stdout: > > + pass > > + except TimeoutError: > > + pass > > + self._ssh_channel.settimeout(self._timeout) # reset timeout > > + > > + def send_command_get_output(self, command: str, prompt: str) -> st= r: > > + """Send a command and get all output before the expected endin= g > 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 =3D "" > > + for line in self._stdout: > > + out +=3D 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..bde3468b > > --- /dev/null > > +++ b/dts/framework/remote_session/remote/testpmd_shell.py > > @@ -0,0 +1,67 @@ > > +# 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 TestPmdShell(InteractiveShell): > > + expected_prompt: str =3D "testpmd>" > > + _eal_flags: str > > + > > + def __init__( > > + self, > > + interactive_session: SSHClient, > > + logger: DTSLOG, > > + path_to_testpmd: PurePath, > > + eal_flags: str, > > + timeout: float =3D SETTINGS.timeout, > > + ) -> None: > > + """Initializes an interactive testpmd session using specified > parameters.""" > > + self._eal_flags =3D eal_flags > > + > > + super(TestPmdShell, self).__init__( > > + interactive_session, > > + logger=3Dlogger, > > + path_to_app=3Dpath_to_testpmd, > > + timeout=3Dtimeout, > > + ) > > + self._start_application() > > + > > + 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 =3D expected_prom= pt) > -> 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[str]: > > + """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 =3D self.send_command("show device info all") > > + dev_list: list[str] =3D [] > > + for line in dev_info.split("\n"): > > + if "device name:" in line.lower(): > > + dev_list.append(line.strip().split(": ")[1].strip()) > > + return dev_list > > diff --git a/dts/framework/test_result.py b/dts/framework/test_result.p= y > > index 74391982..2436eb7f 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) 2022-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] =3D 0 > > self["PASS RATE"] =3D 0.0 > > - self["DPDK VERSION"] =3D dpdk_version > > + if output_info: > > + for info_key, info_val in output_info.items(): > > + self[info_key] =3D 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 =3D build_target.os > > self.cpu =3D build_target.cpu > > self.compiler =3D build_target.compiler > > + self.compiler_version =3D None > > + self.dpdk_version =3D None > > + > > + def add_build_target_versions(self, versions: BuildTargetInfo) -> > None: > > + self.compiler_version =3D versions.compiler_version > > + self.dpdk_version =3D versions.dpdk_version > > > > def add_test_suite(self, test_suite_name: str) -> TestSuiteResult: > > test_suite_result =3D 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 =3D sut_node > > + self.sut_version_info =3D sut_version_info > > + self.sut_os_name =3D sut_version_info.os_name > > + self.sut_os_version =3D sut_version_info.os_version > > + self.sut_kernel_version =3D 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 =3D None > > + self.output =3D None > > self._logger =3D logger > > self._errors =3D [] > > self._return_code =3D ErrorSeverity.NO_ERR > > self._stats_result =3D None > > self._stats_filename =3D os.path.join(SETTINGS.output_dir, > "statistics.txt") > > > > - def add_execution(self, sut_node: NodeConfiguration) -> > ExecutionResult: > > - execution_result =3D ExecutionResult(sut_node) > > + def add_execution( > > + self, sut_node: NodeConfiguration, sut_version_info: NodeInfo > > + ) -> ExecutionResult: > > + execution_result =3D ExecutionResult(sut_node, sut_version_inf= o) > > 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 =3D Statistics(self.dpdk_version) > > + self._stats_result =3D 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..5df5d2a6 100644 > > --- a/dts/framework/test_suite.py > > +++ b/dts/framework/test_suite.py > > @@ -11,10 +11,21 @@ > > 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 > > +from .test_result import ( > > + BuildTargetResult, > > + DTSResult, > > + Result, > > + TestCaseResult, > > + TestSuiteResult, > > +) > > from .testbed_model import SutNode > > > > > > @@ -37,10 +48,12 @@ class TestSuite(object): > > """ > > > > sut_node: SutNode > > + is_blocking =3D False > > _logger: DTSLOG > > _test_cases_to_run: list[str] > > _func: bool > > _result: TestSuiteResult > > + _dts_result: DTSResult > > > > def __init__( > > self, > > @@ -48,6 +61,7 @@ def __init__( > > test_cases: list[str], > > func: bool, > > build_target_result: BuildTargetResult, > > + dts_result: DTSResult, > > ): > > self.sut_node =3D sut_node > > self._logger =3D getLogger(self.__class__.__name__) > > @@ -55,6 +69,7 @@ def __init__( > > self._test_cases_to_run.extend(SETTINGS.test_cases) > > self._func =3D func > > self._result =3D > build_target_result.add_test_suite(self.__class__.__name__) > > + self._dts_result =3D dts_result > > > > def set_up_suite(self) -> None: > > """ > > @@ -118,6 +133,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 =3D 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 =3D 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..9b17ac3d 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) 2022-2023 University of New Hampshire > > > > import os > > import tarfile > > import time > > from pathlib import PurePath > > - > > -from framework.config import BuildTargetConfiguration, NodeConfigurati= on > > -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=3D'vf' > > + :param no_pci: switch of disable PCI bus eg: > > + no_pci=3DTrue > > + :param vdevs: virtual device list, eg: > > + vdevs=3D[ > > + VirtualDevice('net_ring0'), > > + VirtualDevice('net_ring1') > > + ] > > + :param other_eal_param: user defined DPDK eal parameters, eg: > > + other_eal_param=3D'--single-file-segments' > > + """ > > + self._lcore_list =3D f"-l {lcore_list}" > > + self._memory_channels =3D f"-n {memory_channels}" > > + self._prefix =3D prefix > > + if prefix: > > + self._prefix =3D f"--file-prefix=3D{prefix}" > > + self._no_pci =3D "--no-pci" if no_pci else "" > > + self._vdevs =3D " ".join(f"--vdev {vdev}" for vdev in vdevs) > > + self._other_eal_param =3D 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, providi= ng > > @@ -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 =3D EnvVarsDict() > > self._remote_tmp_dir =3D self.main_session.get_remote_tmp_dir(= ) > > self.__remote_dpdk_dir =3D None > > - self._dpdk_version =3D None > > self._app_compile_timeout =3D 90 > > self._dpdk_kill_session =3D None > > self._dpdk_timestamp =3D ( > > f"{str(os.getpid())}_{time.strftime('%Y%m%d%H%M%S', > time.localtime())}" > > ) > > + self._dpdk_version =3D None > > + self._node_info =3D None > > + self._compiler_version =3D 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 =3D 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 =3D > 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=3Dself.dpdk_version, > compiler_version=3Dself.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 =3D None > > + self._compiler_version =3D None > > self._configure_build_target(build_target_config) > > self._copy_dpdk_tarball() > > self._build_dpdk() > > @@ -262,48 +355,43 @@ def run_dpdk_app( > > f"{app_path} {eal_args}", timeout, verify=3DTrue > > ) > > > > - > > -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, > > + path_to_app: PurePath | None =3D None, > > + timeout: float =3D SETTINGS.timeout, > > + eal_parameters: EalParameters | None =3D 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 applicatio= n. > > + path_to_app: Represents a path to the application you are > attempting to > > + launch. This path will be executed at the start of the > app > > + initialization. If one isn't provided, the default > specified in the > > + enumeration will be used. > > + 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 th= e > app. 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=3D'vf' > > - :param no_pci: switch of disable PCI bus eg: > > - no_pci=3DTrue > > - :param vdevs: virtual device list, eg: > > - vdevs=3D[ > > - VirtualDevice('net_ring0'), > > - VirtualDevice('net_ring1') > > - ] > > - :param other_eal_param: user defined DPDK eal parameters, eg: > > - other_eal_param=3D'--single-file-segments' > > - """ > > - self._lcore_list =3D f"-l {lcore_list}" > > - self._memory_channels =3D f"-n {memory_channels}" > > - self._prefix =3D prefix > > - if prefix: > > - self._prefix =3D f"--file-prefix=3D{prefix}" > > - self._no_pci =3D "--no-pci" if no_pci else "" > > - self._vdevs =3D " ".join(f"--vdev {vdev}" for vdev in vdevs) > > - self._other_eal_param =3D other_eal_param > > + # if we just want a default shell, there is no need to append > the DPDK build > > + # directory to the path > > + default_path =3D shell_type.get_path() > > > > - 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 shell_type !=3D InteractiveApp.shell: > > + default_path =3D self.main_session.join_remote_path( > > + self.remote_dpdk_build_dir, shell_type.get_path() > > + ) > > + return > self.main_session.interactive_session.create_interactive_shell( > > + shell_type, > > + path_to_app if path_to_app else default_path, > > + str(eal_parameters) if eal_parameters else "", > > + timeout, > > ) > > I forgot to mention that I'd like to change to structure a bit here - > calling self.main_session.create_interactive_shell() would makes more > sense to me, as the interactive_session is basically an implementation > detail. > Sure, I'll make that change as well. > > > diff --git a/dts/tests/TestSuite_smoke_tests.py > b/dts/tests/TestSuite_smoke_tests.py > > new file mode 100644 > > index 00000000..b7e70ee1 > > --- /dev/null > > +++ b/dts/tests/TestSuite_smoke_tests.py > > @@ -0,0 +1,118 @@ > > +# SPDX-License-Identifier: BSD-3-Clause > > +# Copyright(c) 2023 University of New Hampshire > > + > > +import re > > + > > +from framework.config import InteractiveApp > > +from framework.remote_session import TestPmdShell > > +from framework.settings import SETTINGS > > +from framework.test_suite import TestSuite > > + > > + > > +class SmokeTests(TestSuite): > > + is_blocking =3D True > > + # dicts in this list are expected to have two keys: > > + # "pci_address" and "current_driver" > > + nics_in_node: list[dict[str, str]] =3D [] > > + > > + def set_up_suite(self) -> None: > > + """ > > + Setup: > > + Set the build directory path and generate a list of NICs i= n > the SUT node. > > + """ > > + self.dpdk_build_dir_path =3D self.sut_node.remote_dpdk_build_d= ir > > + for nic in self.sut_node.config.ports: > > + new_dict =3D { > > + "pci_address": nic.pci, > > + "current_driver": nic.os_driver.strip(), > > + } > > + self.nics_in_node.append(new_dict) > > + > > + 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=3DTrue, > > + ) > > + > > + def test_driver_tests(self) -> None: > > + """ > > + Test: > > + Run the driver-test unit-test suite through meson. > > + """ > > + list_of_vdevs =3D "" > > + for dev in self.sut_node._execution_config.vdevs: > > + list_of_vdevs +=3D f"--vdev {dev} " > > + list_of_vdevs =3D 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=3DTrue, > > + ) > > + else: > > + self.sut_node.main_session.send_command( > > + f"meson test -C {self.dpdk_build_dir_path} --suite > driver-tests", > > + 300, > > + verify=3DTrue, > > + ) > > + > > + def test_devices_listed_in_testpmd(self) -> None: > > + """ > > + Test: > > + Uses testpmd driver to verify that devices have been found > by testpmd. > > + """ > > + testpmd_driver =3D > 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[str] =3D testpmd_driver.get_devices() > > + for nic in self.nics_in_node: > > + self.verify( > > + nic["pci_address"] in dev_list, > > + f"Device {nic['pci_address']} 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 =3D self.sut_node.main_session.join_remote_pat= h( > > + self.sut_node._remote_dpdk_dir, "usertools", > "dpdk-devbind.py" > > + ) > > + > > + regex_for_pci_address =3D "/[0-9]{4}:[0-9]{2}:[0-9]{2}.[0-9]{1= }/" > > This shouldn't be tucked away in a test case. It should be in some > high-level module - let's put it to utils.py for the time being. > Good point, this could be something that is useful in many other places as well. I just created it there because that's just where I needed it but it would be good to allow other people to reach it easily. > > > + all_nics_in_dpdk_devbind =3D > 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 =3D re.search( > > + f"{nic['pci_address']}[^\\n]*drv=3D([\\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_address']}) 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) =3D=3D nic["current_driv= er"], > > + f"Driver for device {nic['pci_address']} does not matc= h > driver listed in " > > + f"configuration (bound to > {devbind_info_for_nic.group(1)})", > > + ) > > -- > > 2.41.0 > > > Thank you for the comments! Jeremy --0000000000003319cb06003a4a96 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable


<= div dir=3D"ltr" class=3D"gmail_attr">On Tue, Jul 11, 2023 at 5:42=E2=80=AFA= M Juraj Linke=C5=A1 <juraj.linkes@pantheon.tech> wrote:
Just a few more comments.

On Mon, Jul 10, 2023 at 6:23=E2=80=AFPM <jspewock@iol.unh.edu> wrote:
>
> From: Jeremy Spewock <jspewock@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<= br> > fail, the DTS execution terminates as part of a "fail-fast" = model.
>
> Signed-off-by: Jeremy Spewock <jspewock@iol.unh.edu>
> ---
>=C2=A0 dts/conf.yaml=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0|=C2= =A0 17 +-
>=C2=A0 dts/framework/config/__init__.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 | 116 +++++++++--
>=C2=A0 dts/framework/config/conf_yaml_schema.json=C2=A0 =C2=A0 | 142 ++= +++++++++++-
>=C2=A0 dts/framework/dts.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 |=C2=A0 88 ++++++---
>=C2=A0 dts/framework/exception.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 |=C2=A0 12 ++
>=C2=A0 dts/framework/remote_session/__init__.py=C2=A0 =C2=A0 =C2=A0 |= =C2=A0 10 +-
>=C2=A0 dts/framework/remote_session/os_session.py=C2=A0 =C2=A0 |=C2=A0 = 24 ++-
>=C2=A0 dts/framework/remote_session/posix_session.py |=C2=A0 29 ++-
>=C2=A0 .../remote_session/remote/__init__.py=C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0|=C2=A0 10 +
>=C2=A0 .../remote/interactive_remote_session.py=C2=A0 =C2=A0 =C2=A0 | 1= 18 ++++++++++++
>=C2=A0 .../remote/interactive_shell.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0|=C2=A0 99 ++++++++++
>=C2=A0 .../remote_session/remote/testpmd_shell.py=C2=A0 =C2=A0 |=C2=A0 = 67 +++++++
>=C2=A0 dts/framework/test_result.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 |=C2=A0 37 +++-
>=C2=A0 dts/framework/test_suite.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0|=C2=A0 21 +-
>=C2=A0 dts/framework/testbed_model/node.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0|=C2=A0 =C2=A02 +
>=C2=A0 dts/framework/testbed_model/sut_node.py=C2=A0 =C2=A0 =C2=A0 =C2= =A0| 180 +++++++++++++-----
>=C2=A0 dts/tests/TestSuite_smoke_tests.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 | 118 ++++++++++++
>=C2=A0 17 files changed, 994 insertions(+), 96 deletions(-)
>=C2=A0 create mode 100644 dts/framework/remote_session/remote/interacti= ve_remote_session.py
>=C2=A0 create mode 100644 dts/framework/remote_session/remote/interacti= ve_shell.py
>=C2=A0 create mode 100644 dts/framework/remote_session/remote/testpmd_s= hell.py
>=C2=A0 create mode 100644 dts/tests/TestSuite_smoke_tests.py
>
> diff --git a/dts/conf.yaml b/dts/conf.yaml
> index a9bd8a3e..2717de13 100644
> --- a/dts/conf.yaml
> +++ b/dts/conf.yaml
> @@ -10,9 +10,13 @@ executions:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 compiler_wrapper: ccache
>=C2=A0 =C2=A0 =C2=A0 perf: false
>=C2=A0 =C2=A0 =C2=A0 func: true
> +=C2=A0 =C2=A0 skip_smoke_tests: false #optional flag that allow you t= o ski smoke tests

Typo: ski
Also put a space after #

>=C2=A0 =C2=A0 =C2=A0 test_suites:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 - hello_world
> -=C2=A0 =C2=A0 system_under_test: "SUT 1"
> +=C2=A0 =C2=A0 system_under_test:
> +=C2=A0 =C2=A0 =C2=A0 node_name: "SUT 1"
> +=C2=A0 =C2=A0 =C2=A0 vdevs: #optional: if removed vdevs won't be = used in the execution

Missing space after #
The sentence after hugepages has a comma in it, let's unify those.

> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 - "crypto_openssl"
>=C2=A0 nodes:
>=C2=A0 =C2=A0 - name: "SUT 1"
>=C2=A0 =C2=A0 =C2=A0 hostname: sut1.change.me.localhost
> @@ -20,6 +24,17 @@ nodes:
>=C2=A0 =C2=A0 =C2=A0 arch: x86_64
>=C2=A0 =C2=A0 =C2=A0 os: linux
>=C2=A0 =C2=A0 =C2=A0 lcores: ""
> +=C2=A0 =C2=A0 ports:

I'm comparing my version with this patch and I've just noticed this= -
let's put the ports at the end (after hugepages). This way we'll ha= ve
the configuration sorted into sections of sorts:
Cores/cpu config
Memory config
Port/devices config

> +=C2=A0 =C2=A0 =C2=A0 - pci: "0000:00:08.0"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 os_driver_for_dpdk: vfio-pci #OS driver t= hat DPDK will use

Missing space after #


Sorry, I'm just used to writing comments = without the space but I'll fix this.

=C2=A0
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 os_driver: i40e
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 peer_node: "TG 1"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 peer_pci: "0000:00:08.0"
> +=C2=A0 =C2=A0 =C2=A0 - pci: "0000:00:08.1"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 os_driver_for_dpdk: vfio-pci
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 os_driver: i40e
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 peer_node: "TG 1"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 peer_pci: "0000:00:08.1"
>=C2=A0 =C2=A0 =C2=A0 use_first_core: false
>=C2=A0 =C2=A0 =C2=A0 memory_channels: 4
>=C2=A0 =C2=A0 =C2=A0 hugepages:=C2=A0 # optional; if removed, will use = system hugepage configuration
> diff --git a/dts/framework/config/__init__.py b/dts/framework/config/_= _init__.py
> index ebb0823f..75ac1cbe 100644
> --- a/dts/framework/config/__init__.py
> +++ b/dts/framework/config/__init__.py
> @@ -12,6 +12,7 @@
>=C2=A0 import pathlib
>=C2=A0 from dataclasses import dataclass
>=C2=A0 from enum import Enum, auto, unique
> +from pathlib import PurePath
>=C2=A0 from typing import Any, TypedDict
>
>=C2=A0 import warlock=C2=A0 # type: ignore
> @@ -72,6 +73,20 @@ class HugepageConfiguration:
>=C2=A0 =C2=A0 =C2=A0 force_first_numa: bool
>
>
> +@dataclass(slots=3DTrue, frozen=3DTrue)
> +class PortConfig:
> +=C2=A0 =C2=A0 node: str
> +=C2=A0 =C2=A0 pci: str
> +=C2=A0 =C2=A0 os_driver_for_dpdk: str
> +=C2=A0 =C2=A0 os_driver: str
> +=C2=A0 =C2=A0 peer_node: str
> +=C2=A0 =C2=A0 peer_pci: str
> +
> +=C2=A0 =C2=A0 @staticmethod
> +=C2=A0 =C2=A0 def from_dict(node: str, d: dict) -> "PortConfi= g":
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return PortConfig(node=3Dnode, **d)
> +
> +
>=C2=A0 @dataclass(slots=3DTrue, frozen=3DTrue)
>=C2=A0 class NodeConfiguration:
>=C2=A0 =C2=A0 =C2=A0 name: str
> @@ -84,6 +99,7 @@ class NodeConfiguration:
>=C2=A0 =C2=A0 =C2=A0 use_first_core: bool
>=C2=A0 =C2=A0 =C2=A0 memory_channels: int
>=C2=A0 =C2=A0 =C2=A0 hugepages: HugepageConfiguration | None
> +=C2=A0 =C2=A0 ports: list[PortConfig]
>
>=C2=A0 =C2=A0 =C2=A0 @staticmethod
>=C2=A0 =C2=A0 =C2=A0 def from_dict(d: dict) -> "NodeConfigurati= on":
> @@ -92,18 +108,43 @@ def from_dict(d: dict) -> "NodeConfigurat= ion":
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if "force_first_n= uma" not in hugepage_config:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepage= _config["force_first_numa"] =3D False
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepage_config =3D Hu= gepageConfiguration(**hugepage_config)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 common_config =3D {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "name": d["n= ame"],
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "hostname": d[&qu= ot;hostname"],
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "user": d["u= ser"],
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "password": d.get= ("password"),
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "arch": Architect= ure(d["arch"]),
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "os": OS(d["= os"]),
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "lcores": d.get(&= quot;lcores", "1"),
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "use_first_core":= d.get("use_first_core", False),
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "memory_channels"= : d.get("memory_channels", 1),
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "hugepages": huge= page_config,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "ports": [PortCon= fig.from_dict(d["name"], port) for port in d["ports"]],=
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 }
> +
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return NodeConfiguration(**common_config)=
> +
> +
> +@dataclass(slots=3DTrue)

Looks like this could be frozen as well. This should work even in the
future, as I imagine we'll get all the info we need just once.

You're right, this data isn't really something= that's going to change once we collect it and it's probably better= to enforce that. I'll make this change.


> +class NodeInfo:
> +=C2=A0 =C2=A0 """Class to hold important versions with= in the node.
> +
> +=C2=A0 =C2=A0 This class, unlike the NodeConfiguration class, cannot = be generated at the start.
> +=C2=A0 =C2=A0 This is because we need to initialize a connection with= the node before we can
> +=C2=A0 =C2=A0 collect the information needed in this class. Therefore= , it cannot be a part of
> +=C2=A0 =C2=A0 the configuration class above.
> +=C2=A0 =C2=A0 """
>
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 return NodeConfiguration(
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 name=3Dd["name"],=
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 hostname=3Dd["hostname= "],
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 user=3Dd["user"],=
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 password=3Dd.get("pass= word"),
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 arch=3DArchitecture(d["= ;arch"]),
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 os=3DOS(d["os"]),=
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 lcores=3Dd.get("lcores= ", "1"),
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 use_first_core=3Dd.get(&quo= t;use_first_core", False),
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 memory_channels=3Dd.get(&qu= ot;memory_channels", 1),
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepages=3Dhugepage_config= ,
> +=C2=A0 =C2=A0 os_name: str
> +=C2=A0 =C2=A0 os_version: str
> +=C2=A0 =C2=A0 kernel_version: str
> +
> +=C2=A0 =C2=A0 @staticmethod
> +=C2=A0 =C2=A0 def from_dict(d: dict):
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return NodeInfo(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 os_name=3Dd["os_name&q= uot;],
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 os_version=3Dd["os_ver= sion"],
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 kernel_version=3Dd["ke= rnel_version"],
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )

We don't need the from_dict method, as we can instantiate this class right away (node_info =3D NodeInfo(os_name=3Dd["os_name"],
os_version=3Dd["os_version"], kernel_version=3Dd["kernel_ver= sion"])).

The other classes need this method because we're doing some processing<= br> before instantiating the classes - this one doesn't need it.

Good point, I included it to make it similar to the othe= rs but it really isn't necessary because I'm not even using it.
=
=C2=A0

>
>
> @@ -128,6 +169,24 @@ def from_dict(d: dict) -> "BuildTargetCon= figuration":
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>
>
> +@dataclass(slots=3DTrue)

Can also be frozen.

> +class BuildTargetInfo:
> +=C2=A0 =C2=A0 """Class to hold important versions with= in the build target.
> +
> +=C2=A0 =C2=A0 This is very similar to the NodeVersionInfo class, it j= ust instead holds information

References renamed class.

Good catch, I totall= y missed the reference to the other class in this comment.
<= div>=C2=A0

> +=C2=A0 =C2=A0 for the build target.
> +=C2=A0 =C2=A0 """
> +
> +=C2=A0 =C2=A0 dpdk_version: str
> +=C2=A0 =C2=A0 compiler_version: str
> +
> +=C2=A0 =C2=A0 @staticmethod
> +=C2=A0 =C2=A0 def from_dict(d: dict):
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return BuildTargetInfo(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 dpdk_version=3Dd["dpdk= _version"], compiler_version=3Dd["compiler_version"]
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 )

Same as above.

Good catches on these as well, = I'll be sure to make the changes.
=C2=A0

> +
> +
>=C2=A0 class TestSuiteConfigDict(TypedDict):
>=C2=A0 =C2=A0 =C2=A0 suite: str
>=C2=A0 =C2=A0 =C2=A0 cases: list[str]
> @@ -157,6 +216,8 @@ class ExecutionConfiguration:
>=C2=A0 =C2=A0 =C2=A0 func: bool
>=C2=A0 =C2=A0 =C2=A0 test_suites: list[TestSuiteConfig]
>=C2=A0 =C2=A0 =C2=A0 system_under_test: NodeConfiguration
> +=C2=A0 =C2=A0 vdevs: list[str]
> +=C2=A0 =C2=A0 skip_smoke_tests: bool
>
>=C2=A0 =C2=A0 =C2=A0 @staticmethod
>=C2=A0 =C2=A0 =C2=A0 def from_dict(d: dict, node_map: dict) -> "= ;ExecutionConfiguration":
> @@ -166,15 +227,20 @@ def from_dict(d: dict, node_map: dict) -> &qu= ot;ExecutionConfiguration":
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 test_suites: list[TestSuiteConfig] = =3D list(
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 map(TestSuiteConfig.fr= om_dict, d["test_suites"])
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 sut_name =3D d["system_under_test&qu= ot;]
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 sut_name =3D d["system_under_test&qu= ot;]["node_name"]
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 skip_smoke_tests =3D d.get("skip_smo= ke_tests", False)
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 assert sut_name in node_map, f"= Unknown SUT {sut_name} in execution {d}"
> -
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 vdevs =3D (
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 d["system_under_test&q= uot;]["vdevs"] if "vdevs" in d["system_under_test&= quot;] else []
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return ExecutionConfiguration(
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 build_targets=3Dbuild_= targets,
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 perf=3Dd["perf&qu= ot;],
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 func=3Dd["func&qu= ot;],
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 skip_smoke_tests=3Dskip_smo= ke_tests,
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 test_suites=3Dtest_sui= tes,
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 system_under_test=3Dno= de_map[sut_name],
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 vdevs=3Dvdevs,
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>
>
> @@ -221,3 +287,27 @@ def load_config() -> Configuration:
>
>
>=C2=A0 CONFIGURATION =3D load_config()
> +
> +
> +@unique
> +class InteractiveApp(Enum):
> +=C2=A0 =C2=A0 """An enum that represents different sup= ported interactive applications
> +
> +=C2=A0 =C2=A0 The values in this enum must all be set to objects that= have a key called
> +=C2=A0 =C2=A0 "default_path" where "default_path"= represents a PurPath object for the path
> +=C2=A0 =C2=A0 to the application. This default path will be passed in= to the handler class
> +=C2=A0 =C2=A0 for the application so that it can start the applicatio= n. For every key other
> +=C2=A0 =C2=A0 than the default shell option, the path will be appende= d to the path to the DPDK
> +=C2=A0 =C2=A0 build directory for the current SUT node.
> +=C2=A0 =C2=A0 """
> +
> +=C2=A0 =C2=A0 shell =3D {"default_path": PurePath()}
> +=C2=A0 =C2=A0 testpmd =3D {"default_path": PurePath("a= pp", "dpdk-testpmd")}
> +
> +=C2=A0 =C2=A0 def get_path(self) -> PurePath:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """A method for getting th= e default paths of an application
> +
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Returns:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 String array that represent= s an OS agnostic path to the application.
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return self.value["default_path"= ;]
> diff --git a/dts/framework/config/conf_yaml_schema.json b/dts/framewor= k/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 @@
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 "type": "string",
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 "description": "A unique ide= ntifier for a node"
>=C2=A0 =C2=A0 =C2=A0 },
> +=C2=A0 =C2=A0 "NIC": {
> +=C2=A0 =C2=A0 =C2=A0 "type": "string",
> +=C2=A0 =C2=A0 =C2=A0 "enum": [
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "ALL",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "ConnectX3_MT4103",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "ConnectX4_LX_MT4117",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "ConnectX4_MT4115",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "ConnectX5_MT4119",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "ConnectX5_MT4121",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "I40E_10G-10G_BASE_T_BC",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "I40E_10G-10G_BASE_T_X722",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "I40E_10G-SFP_X722",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "I40E_10G-SFP_XL710",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "I40E_10G-X722_A0",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "I40E_1G-1G_BASE_T_X722",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "I40E_25G-25G_SFP28",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "I40E_40G-QSFP_A",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "I40E_40G-QSFP_B",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IAVF-ADAPTIVE_VF",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IAVF-VF",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IAVF_10G-X722_VF",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "ICE_100G-E810C_QSFP",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "ICE_25G-E810C_SFP",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "ICE_25G-E810_XXV_SFP",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB-I350_VF",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-82540EM",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-82545EM_COPPER",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-82571EB_COPPER",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-82574L",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-82576",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-82576_QUAD_COPPER",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-82576_QUAD_COPPER_ET2",=
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-82580_COPPER",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-I210_COPPER",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-I350_COPPER",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-I354_SGMII",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-PCH_LPTLP_I218_LM",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-PCH_LPTLP_I218_V",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-PCH_LPT_I217_LM",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-PCH_LPT_I217_V",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_2.5G-I354_BACKPLANE_2_5GBPS&quo= t;,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGC-I225_LM",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGC-I226_LM",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-82599_SFP",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-82599_SFP_SF_QP", > +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-82599_T3_LOM",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-82599_VF",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-X540T",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-X540_VF",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-X550EM_A_SFP",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-X550EM_X_10G_T",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-X550EM_X_SFP",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-X550EM_X_VF",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-X550T",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-X550_VF",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "brcm_57414",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "brcm_P2100G",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "cavium_0011",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "cavium_a034",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "cavium_a063",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "cavium_a064",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "fastlinq_ql41000",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "fastlinq_ql41000_vf",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "fastlinq_ql45000",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "fastlinq_ql45000_vf",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "hi1822",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "virtio"
> +=C2=A0 =C2=A0 =C2=A0 ]
> +=C2=A0 =C2=A0 },
> +
>=C2=A0 =C2=A0 =C2=A0 "ARCH": {
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 "type": "string",
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 "enum": [
> @@ -94,6 +164,19 @@
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "amount"
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 ]
>=C2=A0 =C2=A0 =C2=A0 },
> +=C2=A0 =C2=A0 "pci_address": {
> +=C2=A0 =C2=A0 =C2=A0 "type": "string",
> +=C2=A0 =C2=A0 =C2=A0 "pattern": "^[\\da-fA-F]{4}:[\\da= -fA-F]{2}:[\\da-fA-F]{2}.\\d:?\\w*$"
> +=C2=A0 =C2=A0 },
> +=C2=A0 =C2=A0 "port_peer_address": {
> +=C2=A0 =C2=A0 =C2=A0 "description": "Peer is a TRex po= rt, and IXIA port or a PCI address",
> +=C2=A0 =C2=A0 =C2=A0 "oneOf": [
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "description": "PCI= peer port",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "$ref": "#/definiti= ons/pci_address"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 }
> +=C2=A0 =C2=A0 =C2=A0 ]
> +=C2=A0 =C2=A0 },
>=C2=A0 =C2=A0 =C2=A0 "test_suite": {
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 "type": "string",
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 "enum": [
> @@ -165,6 +248,44 @@
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "hugepages": {
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "$ref": &quo= t;#/definitions/hugepages"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "ports": {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "type": "arr= ay",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "items": {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "type": &q= uot;object",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "description&qu= ot;: "Each port should be described on both sides of the connection. T= his makes configuration slightly more verbose but greatly simplifies implem= entation. 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 i= s connected to port 1, node 2, but port 1, node 2 says it is connected to p= ort 2, node 1.",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "properties&quo= t;: {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "pci&quo= t;: {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "= $ref": "#/definitions/pci_address",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "= description": "The local PCI address of the port"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "os_driv= er_for_dpdk": {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "= type": "string",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "= description": "The driver that the kernel should bind this device= to for DPDK to use it. (ex: vfio-pci)"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "os_driv= er": {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "= type": "string",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "= description": "The driver normally used by this port (ex: i40e)&q= uot;
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "peer_no= de": {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "= type": "string",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "= description": "The name of the node the peer port is on"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "peer_pc= i": {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "= $ref": "#/definitions/pci_address",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "= description": "The PCI address of the peer port"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 }
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "additionalProp= erties": false,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "required"= : [
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "pci&quo= t;,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "os_driv= er_for_dpdk",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "os_driv= er",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "peer_no= de",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "peer_pc= i"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ]
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "minimum": 1
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 }
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "additionalProperties": fa= lse,
> @@ -211,8 +332,27 @@
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ]
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 }
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "skip_smoke_tests": { > +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "description": &q= uot;Optional field that allows you to skip smoke testing",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "type": "boo= lean"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "system_under_test"= : {
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "$ref": "#/d= efinitions/node_name"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "type":"obje= ct",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "properties": { > +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "node_name"= ;: {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "$ref&qu= ot;: "#/definitions/node_name"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "vdevs": {=
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "descrip= tion": "Opentional list of names of vdevs to be used in execution= ",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "type&qu= ot;: "array",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "items&q= uot;: {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "= type": "string"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 }
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 }
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "required": [
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "node_name"= ;
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ]
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 }
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "additionalProperties": fa= lse,
> diff --git a/dts/framework/dts.py b/dts/framework/dts.py
> index 05022845..1b67938f 100644
> --- a/dts/framework/dts.py
> +++ b/dts/framework/dts.py
> @@ -5,7 +5,13 @@
>
>=C2=A0 import sys
>
> -from .config import CONFIGURATION, BuildTargetConfiguration, Executio= nConfiguration
> +from .config import (
> +=C2=A0 =C2=A0 CONFIGURATION,
> +=C2=A0 =C2=A0 BuildTargetConfiguration,
> +=C2=A0 =C2=A0 ExecutionConfiguration,
> +=C2=A0 =C2=A0 TestSuiteConfig,
> +)
> +from .exception import BlockingTestSuiteError
>=C2=A0 from .logger import DTSLOG, getLogger
>=C2=A0 from .test_result import BuildTargetResult, DTSResult, Execution= Result, Result
>=C2=A0 from .test_suite import get_test_suites
> @@ -82,7 +88,7 @@ def _run_execution(
>=C2=A0 =C2=A0 =C2=A0 running all build targets in the given execution.<= br> >=C2=A0 =C2=A0 =C2=A0 """
>=C2=A0 =C2=A0 =C2=A0 dts_logger.info(f"Running execution with SUT= '{execution.system_under_test.name}'.")
> -=C2=A0 =C2=A0 execution_result =3D result.add_execution(sut_node.conf= ig)
> +=C2=A0 =C2=A0 execution_result =3D result.add_execution(sut_node.conf= ig, sut_node.node_info)
>
>=C2=A0 =C2=A0 =C2=A0 try:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 sut_node.set_up_execution(execution)=
> @@ -118,14 +124,15 @@ def _run_build_target(
>
>=C2=A0 =C2=A0 =C2=A0 try:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 sut_node.set_up_build_target(build_t= arget)
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 result.dpdk_version =3D sut_node.dpdk_ver= sion
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 # result.dpdk_version =3D sut_node.dpdk_v= ersion
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 build_target_result.add_build_target_vers= ions(sut_node.get_build_target_info())
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 build_target_result.update_setup(Res= ult.PASS)
>=C2=A0 =C2=A0 =C2=A0 except Exception as e:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 dts_logger.exception("Build tar= get setup failed.")
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 build_target_result.update_setup(Res= ult.FAIL, e)
>
>=C2=A0 =C2=A0 =C2=A0 else:
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 _run_suites(sut_node, execution, build_ta= rget_result)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 _run_all_suites(sut_node, execution, buil= d_target_result)
>
>=C2=A0 =C2=A0 =C2=A0 finally:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
> @@ -136,7 +143,7 @@ def _run_build_target(
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 build_target_result.up= date_teardown(Result.FAIL, e)
>
>
> -def _run_suites(
> +def _run_all_suites(
>=C2=A0 =C2=A0 =C2=A0 sut_node: SutNode,
>=C2=A0 =C2=A0 =C2=A0 execution: ExecutionConfiguration,
>=C2=A0 =C2=A0 =C2=A0 build_target_result: BuildTargetResult,
> @@ -146,27 +153,62 @@ def _run_suites(
>=C2=A0 =C2=A0 =C2=A0 with possibly only a subset of test cases.
>=C2=A0 =C2=A0 =C2=A0 If no subset is specified, run all test cases.
>=C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 end_build_target =3D False
> +=C2=A0 =C2=A0 if not execution.skip_smoke_tests:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 execution.test_suites[:0] =3D [TestSuiteC= onfig.from_dict("smoke_tests")]
>=C2=A0 =C2=A0 =C2=A0 for test_suite_config in execution.test_suites: >=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 full_suite_path =3D f"= tests.TestSuite_{test_suite_config.test_suite}"
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 test_suite_classes =3D get_= test_suites(full_suite_path)
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 suites_str =3D ", &quo= t;.join((x.__name__ for x in test_suite_classes))
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 dts_logger.debug(
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"Found = test suites '{suites_str}' in '{full_suite_path}'." > +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 _run_single_suite(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 sut_node, exe= cution, build_target_result, test_suite_config
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception as e:
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 dts_logger.exception("= An error occurred when searching for test suites.")
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 result.update_setup(Result.= ERROR, e)
> -
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for test_suite_class in tes= t_suite_classes:
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 test_suite = =3D test_suite_class(
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= sut_node,
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= test_suite_config.test_cases,
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= execution.func,
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= build_target_result,
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 test_suite.ru= n()
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 except BlockingTestSuiteError as e:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 dts_logger.exception(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"An err= or occurred within {test_suite_config.test_suite}. "
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "Skippin= g build target..."
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 result.add_error(e)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 end_build_target =3D True > +=C2=A0 =C2=A0 =C2=A0 =C2=A0 # if a blocking test failed and we need t= o bail out of suite executions
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 if end_build_target:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 break
> +
> +
> +def _run_single_suite(
> +=C2=A0 =C2=A0 sut_node: SutNode,
> +=C2=A0 =C2=A0 execution: ExecutionConfiguration,
> +=C2=A0 =C2=A0 build_target_result: BuildTargetResult,
> +=C2=A0 =C2=A0 test_suite_config: TestSuiteConfig,
> +) -> None:
> +=C2=A0 =C2=A0 """Runs a single test suite.
> +
> +=C2=A0 =C2=A0 Args:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 sut_node: Node to run tests on.
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 execution: Execution the test case belong= s to.
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 build_target_result: Build target configu= ration test case is run on
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 test_suite_config: Test suite configurati= on
> +
> +=C2=A0 =C2=A0 Raises:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 BlockingTestSuiteError: If a test suite t= hat was marked as blocking fails.
> +=C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 try:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 full_suite_path =3D f"tests.TestSuit= e_{test_suite_config.test_suite}"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 test_suite_classes =3D get_test_suites(fu= ll_suite_path)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 suites_str =3D ", ".join((x.__n= ame__ for x in test_suite_classes))
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 dts_logger.debug(f"Found test suites= '{suites_str}' in '{full_suite_path}'.")
> +=C2=A0 =C2=A0 except Exception as e:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 dts_logger.exception("An error occur= red when searching for test suites.")
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 result.update_setup(Result.ERROR, e)
> +
> +=C2=A0 =C2=A0 else:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 for test_suite_class in test_suite_classe= s:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 test_suite =3D test_suite_c= lass(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 sut_node,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 test_suite_co= nfig.test_cases,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 execution.fun= c,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 build_target_= result,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 result,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 test_suite.run()
>
>
>=C2=A0 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):
>=C2=A0 =C2=A0 =C2=A0 SSH_ERR =3D 4
>=C2=A0 =C2=A0 =C2=A0 DPDK_BUILD_ERR =3D 10
>=C2=A0 =C2=A0 =C2=A0 TESTCASE_VERIFY_ERR =3D 20
> +=C2=A0 =C2=A0 BLOCKING_TESTSUITE_ERR =3D 25
>
>
>=C2=A0 class DTSError(Exception):
> @@ -144,3 +145,14 @@ def __init__(self, value: str):
>
>=C2=A0 =C2=A0 =C2=A0 def __str__(self) -> str:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return repr(self.value)
> +
> +
> +class BlockingTestSuiteError(DTSError):
> +=C2=A0 =C2=A0 suite_name: str
> +=C2=A0 =C2=A0 severity: ClassVar[ErrorSeverity] =3D ErrorSeverity.BLO= CKING_TESTSUITE_ERR
> +
> +=C2=A0 =C2=A0 def __init__(self, suite_name: str) -> None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.suite_name =3D suite_name
> +
> +=C2=A0 =C2=A0 def __str__(self) -> str:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 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..4fe32d35 100644
> --- a/dts/framework/remote_session/__init__.py
> +++ b/dts/framework/remote_session/__init__.py
> @@ -1,5 +1,6 @@
>=C2=A0 # SPDX-License-Identifier: BSD-3-Clause
>=C2=A0 # Copyright(c) 2023 PANTHEON.tech s.r.o.
> +# Copyright(c) 2023 University of New Hampshire
>
>=C2=A0 """
>=C2=A0 The package provides modules for managing remote connections to = a remote host (node),
> @@ -17,7 +18,14 @@
>
>=C2=A0 from .linux_session import LinuxSession
>=C2=A0 from .os_session import OSSession
> -from .remote import CommandResult, RemoteSession, SSHSession
> +from .remote import (
> +=C2=A0 =C2=A0 CommandResult,
> +=C2=A0 =C2=A0 InteractiveRemoteSession,
> +=C2=A0 =C2=A0 InteractiveShell,
> +=C2=A0 =C2=A0 RemoteSession,
> +=C2=A0 =C2=A0 SSHSession,
> +=C2=A0 =C2=A0 TestPmdShell,
> +)
>
>
>=C2=A0 def create_session(
> diff --git a/dts/framework/remote_session/os_session.py b/dts/framewor= k/remote_session/os_session.py
> index 4c48ae25..4346ecc4 100644
> --- a/dts/framework/remote_session/os_session.py
> +++ b/dts/framework/remote_session/os_session.py
> @@ -6,13 +6,19 @@
>=C2=A0 from collections.abc import Iterable
>=C2=A0 from pathlib import PurePath
>
> -from framework.config import Architecture, NodeConfiguration
> +from framework.config import Architecture, NodeConfiguration, NodeInf= o
>=C2=A0 from framework.logger import DTSLOG
>=C2=A0 from framework.settings import SETTINGS
>=C2=A0 from framework.testbed_model import LogicalCore
>=C2=A0 from framework.utils import EnvVarsDict, MesonArgs
>
> -from .remote import CommandResult, RemoteSession, create_remote_sessi= on
> +from .remote import (
> +=C2=A0 =C2=A0 CommandResult,
> +=C2=A0 =C2=A0 InteractiveRemoteSession,
> +=C2=A0 =C2=A0 RemoteSession,
> +=C2=A0 =C2=A0 create_interactive_session,
> +=C2=A0 =C2=A0 create_remote_session,
> +)
>
>
>=C2=A0 class OSSession(ABC):
> @@ -26,6 +32,7 @@ class OSSession(ABC):
>=C2=A0 =C2=A0 =C2=A0 name: str
>=C2=A0 =C2=A0 =C2=A0 _logger: DTSLOG
>=C2=A0 =C2=A0 =C2=A0 remote_session: RemoteSession
> +=C2=A0 =C2=A0 interactive_session: InteractiveRemoteSession
>
>=C2=A0 =C2=A0 =C2=A0 def __init__(
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
> @@ -37,6 +44,7 @@ def __init__(
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.name =3D name
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger =3D logger
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session =3D create_remot= e_session(node_config, name, logger)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.interactive_session =3D create_inter= active_session(node_config, name, logger)
>
>=C2=A0 =C2=A0 =C2=A0 def close(self, force: bool =3D False) -> None:=
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> @@ -173,3 +181,15 @@ def setup_hugepages(self, hugepage_amount: int, f= orce_first_numa: bool) -> None:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if needed and mount the hugepages if= needed.
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 If force_first_numa is True, configu= re hugepages just on the first socket.
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +
> +=C2=A0 =C2=A0 @abstractmethod
> +=C2=A0 =C2=A0 def get_compiler_version(self, compiler_name: str) ->= ; str:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Get installed version of compiler used fo= r DPDK
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +
> +=C2=A0 =C2=A0 @abstractmethod
> +=C2=A0 =C2=A0 def get_node_info(self) -> NodeInfo:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Collect information about the node
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> diff --git a/dts/framework/remote_session/posix_session.py b/dts/frame= work/remote_session/posix_session.py
> index d38062e8..f8ec159f 100644
> --- a/dts/framework/remote_session/posix_session.py
> +++ b/dts/framework/remote_session/posix_session.py
> @@ -6,7 +6,7 @@
>=C2=A0 from collections.abc import Iterable
>=C2=A0 from pathlib import PurePath, PurePosixPath
>
> -from framework.config import Architecture
> +from framework.config import Architecture, NodeInfo
>=C2=A0 from framework.exception import DPDKBuildError, RemoteCommandExe= cutionError
>=C2=A0 from framework.settings import SETTINGS
>=C2=A0 from framework.utils import EnvVarsDict, MesonArgs
> @@ -219,3 +219,30 @@ def _remove_dpdk_runtime_dirs(
>
>=C2=A0 =C2=A0 =C2=A0 def get_dpdk_file_prefix(self, dpdk_prefix) -> = str:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return ""
> +
> +=C2=A0 =C2=A0 def get_compiler_version(self, compiler_name: str) ->= ; str:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 match compiler_name:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 case "gcc":
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return self.s= end_command(f"{compiler_name} --version", 60).stdout.split(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= "\n"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )[0]

The timeouts are still there.

> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 case "clang":
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return self.s= end_command(f"{compiler_name} --version", 60).stdout.split(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= "\n"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )[0]
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 case "msvc":
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return self.s= end_command("cl", 60).stdout
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 case "icc":
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return self.s= end_command(f"{compiler_name} -V", 60).stdout
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 case _:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise ValueEr= ror(f"Unknown compiler {compiler_name}")
> +
> +=C2=A0 =C2=A0 def get_node_info(self) -> NodeInfo:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 os_release_info =3D self.send_command( > +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "awk -F=3D '$1 ~ /= ^NAME$|^VERSION$/ {print $2}' /etc/os-release",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 SETTINGS.timeout,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout.split("\n")
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 kernel_version =3D self.send_command(&quo= t;uname -r", SETTINGS.timeout).stdout
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return NodeInfo(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 os_release_info[0].strip(),= os_release_info[1].strip(), kernel_version
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> diff --git a/dts/framework/remote_session/remote/__init__.py b/dts/fra= mework/remote_session/remote/__init__.py
> index 8a151221..224598a8 100644
> --- a/dts/framework/remote_session/remote/__init__.py
> +++ b/dts/framework/remote_session/remote/__init__.py
> @@ -1,16 +1,26 @@
>=C2=A0 # SPDX-License-Identifier: BSD-3-Clause
>=C2=A0 # Copyright(c) 2023 PANTHEON.tech s.r.o.
> +# Copyright(c) 2022-2023 University of New Hampshire

There are other instances of the copyright statement. It's not
necessary to change them all to just 2023, but I'd say it's
preferable.

I agree. Knowing more about them a= fter your other comments, this should also be changed.
=C2= =A0

>
>=C2=A0 # pylama:ignore=3DW0611
>
>=C2=A0 from framework.config import NodeConfiguration
>=C2=A0 from framework.logger import DTSLOG
>
> +from .interactive_remote_session import InteractiveRemoteSession
> +from .interactive_shell import InteractiveShell
>=C2=A0 from .remote_session import CommandResult, RemoteSession
>=C2=A0 from .ssh_session import SSHSession
> +from .testpmd_shell import TestPmdShell
>
>
>=C2=A0 def create_remote_session(
>=C2=A0 =C2=A0 =C2=A0 node_config: NodeConfiguration, name: str, logger:= DTSLOG
>=C2=A0 ) -> RemoteSession:
>=C2=A0 =C2=A0 =C2=A0 return SSHSession(node_config, name, logger)
> +
> +
> +def create_interactive_session(
> +=C2=A0 =C2=A0 node_config: NodeConfiguration, name: str, logger: DTSL= OG
> +) -> InteractiveRemoteSession:
> +=C2=A0 =C2=A0 return InteractiveRemoteSession(node_config, logger) > diff --git a/dts/framework/remote_session/remote/interactive_remote_se= ssion.py b/dts/framework/remote_session/remote/interactive_remote_session.p= y
> new file mode 100644
> index 00000000..e145d35d
> --- /dev/null
> +++ b/dts/framework/remote_session/remote/interactive_remote_session.p= y
> @@ -0,0 +1,118 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2023 University of New Hampshire
> +
> +import socket
> +import traceback
> +from pathlib import PurePath
> +from typing import Union
> +
> +from paramiko import AutoAddPolicy, SSHClient, Transport=C2=A0 # type= : ignore
> +from paramiko.ssh_exception import (=C2=A0 # type: ignore
> +=C2=A0 =C2=A0 AuthenticationException,
> +=C2=A0 =C2=A0 BadHostKeyException,
> +=C2=A0 =C2=A0 NoValidConnectionsError,
> +=C2=A0 =C2=A0 SSHException,
> +)
> +
> +from framework.config import InteractiveApp, NodeConfiguration
> +from framework.exception import SSHConnectionError
> +from framework.logger import DTSLOG
> +
> +from .interactive_shell import InteractiveShell
> +from .testpmd_shell import TestPmdShell
> +
> +
> +class InteractiveRemoteSession:
> +=C2=A0 =C2=A0 hostname: str
> +=C2=A0 =C2=A0 ip: str
> +=C2=A0 =C2=A0 port: int
> +=C2=A0 =C2=A0 username: str
> +=C2=A0 =C2=A0 password: str
> +=C2=A0 =C2=A0 _logger: DTSLOG
> +=C2=A0 =C2=A0 _node_config: NodeConfiguration
> +=C2=A0 =C2=A0 session: SSHClient
> +=C2=A0 =C2=A0 _transport: Transport | None
> +
> +=C2=A0 =C2=A0 def __init__(self, node_config: NodeConfiguration, _log= ger: DTSLOG) -> None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._node_config =3D node_config
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger =3D _logger
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.hostname =3D node_config.hostname > +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.username =3D node_config.user
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.password =3D node_config.password if= node_config.password else ""
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 port =3D "22"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.ip =3D node_config.hostname
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 if ":" in node_config.hostname:=
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.ip, port =3D node_conf= ig.hostname.split(":")
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.port =3D int(port)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.info(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"Initializing interac= tive connection for {self.username}@{self.hostname}"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._connect()
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.info(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"Interactive connecti= on successful for {self.username}@{self.hostname}"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +
> +=C2=A0 =C2=A0 def _connect(self) -> None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 client =3D SSHClient()
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 client.set_missing_host_key_policy(AutoAd= dPolicy)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session =3D client
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 retry_attempts =3D 10
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 for retry_attempt in range(retry_attempts= ):
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 client.connec= t(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= self.ip,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= username=3Dself.username,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= port=3Dself.port,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= password=3Dself.password,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= timeout=3D20 if self.port else 10,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 except (TypeError, BadHostK= eyException, AuthenticationException) as e:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.= exception(e)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHConn= ectionError(self.hostname) from e
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 except (NoValidConnectionsE= rror, socket.error, SSHException) as e:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.= debug(traceback.format_exc())
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.= warning(e)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.info(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= "Retrying interactive session connection: "
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= f"retry number {retry_attempt +1}"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 break
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHConnectionError(se= lf.hostname)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 # Interactive sessions are used on an &qu= ot;as needed" basis so we have
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 # to set a keepalive
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._transport =3D self.session.get_tran= sport()
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 if self._transport is not None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._transport.set_keepali= ve(30)
> +
> +=C2=A0 =C2=A0 def create_interactive_shell(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 shell_type: InteractiveApp,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 path_to_app: PurePath,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 eal_parameters: str,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout: float,
> +=C2=A0 =C2=A0 ) -> Union[InteractiveShell, TestPmdShell]:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 See "create_interactive_shell" = in SutNode
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 match (shell_type):
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 case InteractiveApp.shell:<= br> > +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return Intera= ctiveShell(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= self.session, self._logger, path_to_app, timeout
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 case InteractiveApp.testpmd= :
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return TestPm= dShell(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= self.session,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= self._logger,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= path_to_app,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= timeout=3Dtimeout,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= eal_flags=3Deal_parameters,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 case _:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._
logger.info(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= f"Unhandled app type {
shell_type.name}, defaulting to shell." > +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return Intera= ctiveShell(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= self.session, self._logger, path_to_app, timeout
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> 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..4b0735c8
> --- /dev/null
> +++ b/dts/framework/remote_session/remote/interactive_shell.py
> @@ -0,0 +1,99 @@
> +from pathlib import PurePath
> +
> +from paramiko import Channel, SSHClient, channel=C2=A0 # type: ignore=
> +
> +from framework.logger import DTSLOG
> +from framework.settings import SETTINGS
> +
> +
> +class InteractiveShell:
> +
> +=C2=A0 =C2=A0 _interactive_session: SSHClient
> +=C2=A0 =C2=A0 _stdin: channel.ChannelStdinFile
> +=C2=A0 =C2=A0 _stdout: channel.ChannelFile
> +=C2=A0 =C2=A0 _ssh_channel: Channel
> +=C2=A0 =C2=A0 _logger: DTSLOG
> +=C2=A0 =C2=A0 _timeout: float
> +=C2=A0 =C2=A0 _path_to_app: PurePath
> +
> +=C2=A0 =C2=A0 def __init__(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 interactive_session: SSHClient,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 logger: DTSLOG,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 path_to_app: PurePath,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout: float =3D SETTINGS.timeout,
> +=C2=A0 =C2=A0 ) -> None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._interactive_session =3D interactive= _session
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._ssh_channel =3D self._interactive_s= ession.invoke_shell()
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._stdin =3D self._ssh_channel.makefil= e_stdin("w")
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._stdout =3D self._ssh_channel.makefi= le("r")
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._ssh_channel.settimeout(timeout)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._ssh_channel.set_combine_stderr(True= )=C2=A0 # combines stdout and stderr streams
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger =3D logger
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._timeout =3D timeout
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._path_to_app =3D path_to_app
> +
> +
> +=C2=A0 =C2=A0 def send_command_no_output(self, command: str) -> No= ne:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Send command to channel= without recording output.
> +
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 This method will not verify any input or = output, it will simply assume the
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 command succeeded. This method will also = consume all output in the buffer
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 after executing the command.
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.info(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"Sending command {com= mand.strip()} and not collecting output"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._stdin.write(f"{command}\n"= ;)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._stdin.flush()
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.empty_stdout_buffer()
> +
> +=C2=A0 =C2=A0 def empty_stdout_buffer(self) -> None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Removes all data from t= he stdout buffer.
> +
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Because of the way paramiko handles read = buffers, there is no way to effectively
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 remove data from, or "flush", r= ead buffers. This method essentially moves our
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 offset on the buffer to the end and thus = "removes" the data from the buffer.
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Timeouts are thrown on read operations of= paramiko pipes based on whether data
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 had been received before timeout so we as= sume that if we reach the timeout then
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 we are at the end of the buffer.
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._ssh_channel.settimeout(0.5)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for line in self._stdout: > +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 pass
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 except TimeoutError:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 pass
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._ssh_channel.settimeout(self._timeou= t)=C2=A0 # reset timeout
> +
> +=C2=A0 =C2=A0 def send_command_get_output(self, command: str, prompt:= str) -> str:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Send a command and get = all output before the expected ending string.
> +
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Lines that expect input are not included = in the stdout buffer so they cannot be
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 used for expect. For example, if you were= prompted to log into something
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 with a username and password, you cannot = expect "username:" because it won't
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 yet be in the stdout buffer. A work aroun= d for this could be consuming an
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 extra newline character to force the curr= ent prompt into the stdout buffer.
> +
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Returns:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 All output in the buffer be= fore expected string
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.info(f"Sending command {c= ommand.strip()}...")
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._stdin.write(f"{command}\n"= ;)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._stdin.flush()
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 out: str =3D ""
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 for line in self._stdout:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out +=3D line
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if prompt in line and not l= ine.rstrip().endswith(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command.rstri= p()
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ):=C2=A0 # ignore line that= sent command
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 break
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.debug(f"Got output: {ou= t}")
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return out
> +
> +=C2=A0 =C2=A0 def close(self) -> None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._stdin.close()
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._ssh_channel.close()
> +
> +=C2=A0 =C2=A0 def __del__(self) -> None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.close()
> diff --git a/dts/framework/remote_session/remote/testpmd_shell.py b/dt= s/framework/remote_session/remote/testpmd_shell.py
> new file mode 100644
> index 00000000..bde3468b
> --- /dev/null
> +++ b/dts/framework/remote_session/remote/testpmd_shell.py
> @@ -0,0 +1,67 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2023 University of New Hampshire
> +
> +
> +from pathlib import PurePath
> +
> +from paramiko import SSHClient=C2=A0 # type: ignore
> +
> +from framework.logger import DTSLOG
> +from framework.settings import SETTINGS
> +
> +from .interactive_shell import InteractiveShell
> +
> +
> +class TestPmdShell(InteractiveShell):
> +=C2=A0 =C2=A0 expected_prompt: str =3D "testpmd>"
> +=C2=A0 =C2=A0 _eal_flags: str
> +
> +=C2=A0 =C2=A0 def __init__(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 interactive_session: SSHClient,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 logger: DTSLOG,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 path_to_testpmd: PurePath,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 eal_flags: str,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout: float =3D SETTINGS.timeout,
> +=C2=A0 =C2=A0 ) -> None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Initializes an interact= ive testpmd session using specified parameters."""
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._eal_flags =3D eal_flags
> +
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 super(TestPmdShell, self).__init__(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 interactive_session,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 logger=3Dlogger,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 path_to_app=3Dpath_to_testp= md,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout=3Dtimeout,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._start_application()
> +
> +=C2=A0 =C2=A0 def _start_application(self) -> None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Starts a new interactiv= e testpmd shell using _path_to_app.
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{self._path_to_app} = {self._eal_flags} -- -i",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +
> +=C2=A0 =C2=A0 def send_command(self, command: str, prompt: str =3D ex= pected_prompt) -> str:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Specific way of handlin= g the command for testpmd
> +
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 An extra newline character is consumed in= order to force the current line into
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 the stdout buffer.
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return self.send_command_get_output(f&quo= t;{command}\n", prompt)
> +
> +=C2=A0 =C2=A0 def get_devices(self) -> list[str]:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Get a list of device na= mes that are known to testpmd
> +
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Uses the device info listed in testpmd an= d then parses the output to
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return only the names of the devices.
> +
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Returns:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 A list of strings represent= ing device names (e.g. 0000:14:00.1)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 dev_info: str =3D self.send_command("= ;show device info all")
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 dev_list: list[str] =3D []
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 for line in dev_info.split("\n"= ):
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if "device name:"= in line.lower():
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 dev_list.appe= nd(line.strip().split(": ")[1].strip())
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return dev_list
> diff --git a/dts/framework/test_result.py b/dts/framework/test_result.= py
> index 74391982..2436eb7f 100644
> --- a/dts/framework/test_result.py
> +++ b/dts/framework/test_result.py
> @@ -1,5 +1,6 @@
>=C2=A0 # SPDX-License-Identifier: BSD-3-Clause
>=C2=A0 # Copyright(c) 2023 PANTHEON.tech s.r.o.
> +# Copyright(c) 2022-2023 University of New Hampshire
>
>=C2=A0 """
>=C2=A0 Generic result container and reporters
> @@ -13,9 +14,11 @@
>=C2=A0 =C2=A0 =C2=A0 OS,
>=C2=A0 =C2=A0 =C2=A0 Architecture,
>=C2=A0 =C2=A0 =C2=A0 BuildTargetConfiguration,
> +=C2=A0 =C2=A0 BuildTargetInfo,
>=C2=A0 =C2=A0 =C2=A0 Compiler,
>=C2=A0 =C2=A0 =C2=A0 CPUType,
>=C2=A0 =C2=A0 =C2=A0 NodeConfiguration,
> +=C2=A0 =C2=A0 NodeInfo,
>=C2=A0 )
>=C2=A0 from .exception import DTSError, ErrorSeverity
>=C2=A0 from .logger import DTSLOG
> @@ -67,12 +70,14 @@ class Statistics(dict):
>=C2=A0 =C2=A0 =C2=A0 Using a dict provides a convenient way to format t= he data.
>=C2=A0 =C2=A0 =C2=A0 """
>
> -=C2=A0 =C2=A0 def __init__(self, dpdk_version):
> +=C2=A0 =C2=A0 def __init__(self, output_info: dict[str, str] | None):=
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 super(Statistics, self).__init__() >=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for result in Result:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self[result.name] =3D 0 >=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self["PASS RATE"] =3D 0.0<= br> > -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self["DPDK VERSION"] =3D dpdk_v= ersion
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 if output_info:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for info_key, info_val in o= utput_info.items():
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self[info_key= ] =3D info_val
>
>=C2=A0 =C2=A0 =C2=A0 def __iadd__(self, other: Result) -> "Stat= istics":
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> @@ -206,6 +211,8 @@ class BuildTargetResult(BaseResult):
>=C2=A0 =C2=A0 =C2=A0 os: OS
>=C2=A0 =C2=A0 =C2=A0 cpu: CPUType
>=C2=A0 =C2=A0 =C2=A0 compiler: Compiler
> +=C2=A0 =C2=A0 compiler_version: str | None
> +=C2=A0 =C2=A0 dpdk_version: str | None
>
>=C2=A0 =C2=A0 =C2=A0 def __init__(self, build_target: BuildTargetConfig= uration):
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 super(BuildTargetResult, self).__ini= t__()
> @@ -213,6 +220,12 @@ def __init__(self, build_target: BuildTargetConfi= guration):
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.os =3D build_target.os
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.cpu =3D build_target.cpu
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.compiler =3D build_target.compi= ler
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.compiler_version =3D None
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.dpdk_version =3D None
> +
> +=C2=A0 =C2=A0 def add_build_target_versions(self, versions: BuildTarg= etInfo) -> None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.compiler_version =3D versions.compil= er_version
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.dpdk_version =3D versions.dpdk_versi= on
>
>=C2=A0 =C2=A0 =C2=A0 def add_test_suite(self, test_suite_name: str) -&g= t; TestSuiteResult:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 test_suite_result =3D TestSuiteResul= t(test_suite_name)
> @@ -228,10 +241,17 @@ class ExecutionResult(BaseResult):
>=C2=A0 =C2=A0 =C2=A0 """
>
>=C2=A0 =C2=A0 =C2=A0 sut_node: NodeConfiguration
> +=C2=A0 =C2=A0 sut_os_name: str
> +=C2=A0 =C2=A0 sut_os_version: str
> +=C2=A0 =C2=A0 sut_kernel_version: str
>
> -=C2=A0 =C2=A0 def __init__(self, sut_node: NodeConfiguration):
> +=C2=A0 =C2=A0 def __init__(self, sut_node: NodeConfiguration, sut_ver= sion_info: NodeInfo):
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 super(ExecutionResult, self).__init_= _()
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sut_node =3D sut_node
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sut_version_info =3D sut_version_inf= o
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sut_os_name =3D sut_version_info.os_= name
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sut_os_version =3D sut_version_info.= os_version
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sut_kernel_version =3D sut_version_i= nfo.kernel_version
>
>=C2=A0 =C2=A0 =C2=A0 def add_build_target(
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self, build_target: BuildTargetConfi= guration
> @@ -258,6 +278,7 @@ class DTSResult(BaseResult):
>=C2=A0 =C2=A0 =C2=A0 """
>
>=C2=A0 =C2=A0 =C2=A0 dpdk_version: str | None
> +=C2=A0 =C2=A0 output: dict | None
>=C2=A0 =C2=A0 =C2=A0 _logger: DTSLOG
>=C2=A0 =C2=A0 =C2=A0 _errors: list[Exception]
>=C2=A0 =C2=A0 =C2=A0 _return_code: ErrorSeverity
> @@ -267,14 +288,17 @@ class DTSResult(BaseResult):
>=C2=A0 =C2=A0 =C2=A0 def __init__(self, logger: DTSLOG):
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 super(DTSResult, self).__init__() >=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.dpdk_version =3D None
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.output =3D None
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger =3D logger
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._errors =3D []
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._return_code =3D ErrorSeverity.= NO_ERR
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._stats_result =3D None
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._stats_filename =3D os.path.joi= n(SETTINGS.output_dir, "statistics.txt")
>
> -=C2=A0 =C2=A0 def add_execution(self, sut_node: NodeConfiguration) -&= gt; ExecutionResult:
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 execution_result =3D ExecutionResult(sut_= node)
> +=C2=A0 =C2=A0 def add_execution(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, sut_node: NodeConfiguration, sut_ve= rsion_info: NodeInfo
> +=C2=A0 =C2=A0 ) -> ExecutionResult:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 execution_result =3D ExecutionResult(sut_= node, sut_version_info)
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._inner_results.append(execution= _result)
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return execution_result
>
> @@ -296,7 +320,8 @@ def process(self) -> None:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for error in self._err= ors:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._lo= gger.debug(repr(error))
>
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._stats_result =3D Statistics(self.dp= dk_version)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._stats_result =3D Statistics(self.ou= tput)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 # add information gathered from the smoke= tests to the statistics
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.add_stats(self._stats_result) >=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 with open(self._stats_filename, &quo= t;w+") as stats_file:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 stats_file.write(str(s= elf._stats_result))
> diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py=
> index 0705f38f..5df5d2a6 100644
> --- a/dts/framework/test_suite.py
> +++ b/dts/framework/test_suite.py
> @@ -11,10 +11,21 @@
>=C2=A0 import re
>=C2=A0 from types import MethodType
>
> -from .exception import ConfigurationError, SSHTimeoutError, TestCaseV= erifyError
> +from .exception import (
> +=C2=A0 =C2=A0 BlockingTestSuiteError,
> +=C2=A0 =C2=A0 ConfigurationError,
> +=C2=A0 =C2=A0 SSHTimeoutError,
> +=C2=A0 =C2=A0 TestCaseVerifyError,
> +)
>=C2=A0 from .logger import DTSLOG, getLogger
>=C2=A0 from .settings import SETTINGS
> -from .test_result import BuildTargetResult, Result, TestCaseResult, T= estSuiteResult
> +from .test_result import (
> +=C2=A0 =C2=A0 BuildTargetResult,
> +=C2=A0 =C2=A0 DTSResult,
> +=C2=A0 =C2=A0 Result,
> +=C2=A0 =C2=A0 TestCaseResult,
> +=C2=A0 =C2=A0 TestSuiteResult,
> +)
>=C2=A0 from .testbed_model import SutNode
>
>
> @@ -37,10 +48,12 @@ class TestSuite(object):
>=C2=A0 =C2=A0 =C2=A0 """
>
>=C2=A0 =C2=A0 =C2=A0 sut_node: SutNode
> +=C2=A0 =C2=A0 is_blocking =3D False
>=C2=A0 =C2=A0 =C2=A0 _logger: DTSLOG
>=C2=A0 =C2=A0 =C2=A0 _test_cases_to_run: list[str]
>=C2=A0 =C2=A0 =C2=A0 _func: bool
>=C2=A0 =C2=A0 =C2=A0 _result: TestSuiteResult
> +=C2=A0 =C2=A0 _dts_result: DTSResult
>
>=C2=A0 =C2=A0 =C2=A0 def __init__(
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
> @@ -48,6 +61,7 @@ def __init__(
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 test_cases: list[str],
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 func: bool,
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 build_target_result: BuildTargetResu= lt,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 dts_result: DTSResult,
>=C2=A0 =C2=A0 =C2=A0 ):
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sut_node =3D sut_node
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger =3D getLogger(self.__cl= ass__.__name__)
> @@ -55,6 +69,7 @@ def __init__(
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._test_cases_to_run.extend(SETTI= NGS.test_cases)
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._func =3D func
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._result =3D build_target_result= .add_test_suite(self.__class__.__name__)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._dts_result =3D dts_result
>
>=C2=A0 =C2=A0 =C2=A0 def set_up_suite(self) -> None:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> @@ -118,6 +133,8 @@ def run(self) -> None:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 f"the next test suite may be affected."
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._re= sult.update_setup(Result.ERROR, e)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if len(self._result.get_err= ors()) > 0 and self.is_blocking:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise Blockin= gTestSuiteError(test_suite_name)
>
>=C2=A0 =C2=A0 =C2=A0 def _execute_test_suite(self) -> None:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testb= ed_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):
>=C2=A0 =C2=A0 =C2=A0 lcores: list[LogicalCore]
>=C2=A0 =C2=A0 =C2=A0 _logger: DTSLOG
>=C2=A0 =C2=A0 =C2=A0 _other_sessions: list[OSSession]
> +=C2=A0 =C2=A0 _execution_config: ExecutionConfiguration
>
>=C2=A0 =C2=A0 =C2=A0 def __init__(self, node_config: NodeConfiguration)= :
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.config =3D node_config
> @@ -64,6 +65,7 @@ def set_up_execution(self, execution_config: Executi= onConfiguration) -> None:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._setup_hugepages()
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._set_up_execution(execution_con= fig)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._execution_config =3D execution_conf= ig
>
>=C2=A0 =C2=A0 =C2=A0 def _set_up_execution(self, execution_config: Exec= utionConfiguration) -> None:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/t= estbed_model/sut_node.py
> index 2b2b50d9..9b17ac3d 100644
> --- a/dts/framework/testbed_model/sut_node.py
> +++ b/dts/framework/testbed_model/sut_node.py
> @@ -1,14 +1,27 @@
>=C2=A0 # SPDX-License-Identifier: BSD-3-Clause
>=C2=A0 # Copyright(c) 2010-2014 Intel Corporation
>=C2=A0 # Copyright(c) 2023 PANTHEON.tech s.r.o.
> +# Copyright(c) 2022-2023 University of New Hampshire
>
>=C2=A0 import os
>=C2=A0 import tarfile
>=C2=A0 import time
>=C2=A0 from pathlib import PurePath
> -
> -from framework.config import BuildTargetConfiguration, NodeConfigurat= ion
> -from framework.remote_session import CommandResult, OSSession
> +from typing import Union
> +
> +from framework.config import (
> +=C2=A0 =C2=A0 BuildTargetConfiguration,
> +=C2=A0 =C2=A0 BuildTargetInfo,
> +=C2=A0 =C2=A0 InteractiveApp,
> +=C2=A0 =C2=A0 NodeConfiguration,
> +=C2=A0 =C2=A0 NodeInfo,
> +)
> +from framework.remote_session import (
> +=C2=A0 =C2=A0 CommandResult,
> +=C2=A0 =C2=A0 InteractiveShell,
> +=C2=A0 =C2=A0 OSSession,
> +=C2=A0 =C2=A0 TestPmdShell,
> +)
>=C2=A0 from framework.settings import SETTINGS
>=C2=A0 from framework.utils import EnvVarsDict, MesonArgs
>
> @@ -16,6 +29,52 @@
>=C2=A0 from .node import Node
>
>
> +class EalParameters(object):
> +=C2=A0 =C2=A0 def __init__(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 lcore_list: LogicalCoreList,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 memory_channels: int,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 prefix: str,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 no_pci: bool,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 vdevs: list[VirtualDevice],
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 other_eal_param: str,
> +=C2=A0 =C2=A0 ):
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Generate eal parameters character string;=
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param lcore_list: the list of logical co= res to use.
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param memory_channels: the number of mem= ory channels to use.
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param prefix: set file prefix string, eg= :
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0 =C2=A0 prefix=3D'vf'
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param no_pci: switch of disable PCI bus = eg:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0 =C2=A0 no_pci=3DTrue
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param vdevs: virtual device list, eg: > +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0 =C2=A0 vdevs=3D[
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0 =C2=A0 =C2=A0 =C2=A0 VirtualDevice('net_ring0'),
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0 =C2=A0 =C2=A0 =C2=A0 VirtualDevice('net_ring1')
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0 =C2=A0 ]
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param other_eal_param: user defined DPDK= eal parameters, eg:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0 =C2=A0 other_eal_param=3D'--single-file-segments'
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._lcore_list =3D f"-l {lcore_lis= t}"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._memory_channels =3D f"-n {memo= ry_channels}"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._prefix =3D prefix
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 if prefix:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._prefix =3D f"--f= ile-prefix=3D{prefix}"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._no_pci =3D "--no-pci" if = no_pci else ""
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._vdevs =3D " ".join(f"= ;--vdev {vdev}" for vdev in vdevs)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._other_eal_param =3D other_eal_param=
> +
> +=C2=A0 =C2=A0 def __str__(self) -> str:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return (
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{self._lcore_list} &= quot;
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{self._memory_channe= ls} "
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{self._prefix} "= ;
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{self._no_pci} "= ;
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{self._vdevs} "=
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{self._other_eal_par= am}"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +
> +
>=C2=A0 class SutNode(Node):
>=C2=A0 =C2=A0 =C2=A0 """
>=C2=A0 =C2=A0 =C2=A0 A class for managing connections to the System und= er Test, providing
> @@ -30,9 +89,11 @@ class SutNode(Node):
>=C2=A0 =C2=A0 =C2=A0 _env_vars: EnvVarsDict
>=C2=A0 =C2=A0 =C2=A0 _remote_tmp_dir: PurePath
>=C2=A0 =C2=A0 =C2=A0 __remote_dpdk_dir: PurePath | None
> -=C2=A0 =C2=A0 _dpdk_version: str | None
>=C2=A0 =C2=A0 =C2=A0 _app_compile_timeout: float
>=C2=A0 =C2=A0 =C2=A0 _dpdk_kill_session: OSSession | None
> +=C2=A0 =C2=A0 _dpdk_version: str | None
> +=C2=A0 =C2=A0 _node_info: NodeInfo | None
> +=C2=A0 =C2=A0 _compiler_version: str | None
>
>=C2=A0 =C2=A0 =C2=A0 def __init__(self, node_config: NodeConfiguration)= :
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 super(SutNode, self).__init__(node_c= onfig)
> @@ -41,12 +102,14 @@ def __init__(self, node_config: NodeConfiguration= ):
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._env_vars =3D EnvVarsDict()
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._remote_tmp_dir =3D self.main_s= ession.get_remote_tmp_dir()
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.__remote_dpdk_dir =3D None
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._dpdk_version =3D None
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._app_compile_timeout =3D 90
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._dpdk_kill_session =3D None
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._dpdk_timestamp =3D (
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{str(os.getpid(= ))}_{time.strftime('%Y%m%d%H%M%S', time.localtime())}"
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._dpdk_version =3D None
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._node_info =3D None
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._compiler_version =3D None
>
>=C2=A0 =C2=A0 =C2=A0 @property
>=C2=A0 =C2=A0 =C2=A0 def _remote_dpdk_dir(self) -> PurePath:
> @@ -75,6 +138,32 @@ def dpdk_version(self) -> str:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return self._dpdk_version
>
> +=C2=A0 =C2=A0 @property
> +=C2=A0 =C2=A0 def node_info(self) -> NodeInfo:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 if self._node_info is None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._node_info =3D self.ma= in_session.get_node_info()
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return self._node_info
> +
> +=C2=A0 =C2=A0 @property
> +=C2=A0 =C2=A0 def compiler_version(self) -> str:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 if self._compiler_version is None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if self._build_target_confi= g is not None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._compile= r_version =3D self.main_session.get_compiler_version(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= self._build_target_config.compiler.name
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.= warning(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= "Failed to get compiler version because"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= "_build_target_config is None."
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return "= "
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return self._compiler_version
> +
> +=C2=A0 =C2=A0 def get_build_target_info(self) -> BuildTargetInfo:<= br> > +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return BuildTargetInfo(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 dpdk_version=3Dself.dpdk_ve= rsion, compiler_version=3Dself.compiler_version
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +
>=C2=A0 =C2=A0 =C2=A0 def _guess_dpdk_remote_dir(self) -> PurePath: >=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return self.main_session.guess_dpdk_= remote_dir(self._remote_tmp_dir)
>
> @@ -84,6 +173,10 @@ def _set_up_build_target(
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Setup DPDK on the SUT node.
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 # we want to ensure that dpdk_version and= compiler_version is reset for new
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 # build targets
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._dpdk_version =3D None
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._compiler_version =3D None
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._configure_build_target(build_t= arget_config)
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._copy_dpdk_tarball()
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._build_dpdk()
> @@ -262,48 +355,43 @@ def run_dpdk_app(
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{app_path} {eal= _args}", timeout, verify=3DTrue
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>
> -
> -class EalParameters(object):
> -=C2=A0 =C2=A0 def __init__(
> +=C2=A0 =C2=A0 def create_interactive_shell(
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 lcore_list: LogicalCoreList,
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 memory_channels: int,
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 prefix: str,
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 no_pci: bool,
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 vdevs: list[VirtualDevice],
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 other_eal_param: str,
> -=C2=A0 =C2=A0 ):
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 shell_type: InteractiveApp,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 path_to_app: PurePath | None =3D None, > +=C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout: float =3D SETTINGS.timeout,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 eal_parameters: EalParameters | None =3D = None,
> +=C2=A0 =C2=A0 ) -> Union[InteractiveShell, TestPmdShell]:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Create a handler for an= interactive session.
> +
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 This method is a factory that calls a met= hod in OSSession to create shells for
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 different DPDK applications.
> +
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 shell_type: Enum value repr= esenting the desired application.
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 path_to_app: Represents a p= ath to the application you are attempting to
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 launch. This = path will be executed at the start of the app
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 initializatio= n. If one isn't provided, the default specified in the
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 enumeration w= ill be used.
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout: Timeout for readin= g output from the SSH channel. If you are
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 reading from = the buffer and don't receive any data within the timeout
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 it will throw= an error.
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 eal_parameters: List of EAL= parameters to use to launch the app. This is
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ignored for b= ase "shell" types.
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Returns:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Instance of the desired int= eractive application.
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 Generate eal parameters character string;=
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param lcore_list: the list of logical co= res to use.
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param memory_channels: the number of mem= ory channels to use.
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param prefix: set file prefix string, eg= :
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0 =C2=A0 prefix=3D'vf'
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param no_pci: switch of disable PCI bus = eg:
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0 =C2=A0 no_pci=3DTrue
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param vdevs: virtual device list, eg: > -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0 =C2=A0 vdevs=3D[
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0 =C2=A0 =C2=A0 =C2=A0 VirtualDevice('net_ring0'),
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0 =C2=A0 =C2=A0 =C2=A0 VirtualDevice('net_ring1')
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0 =C2=A0 ]
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param other_eal_param: user defined DPDK= eal parameters, eg:
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0 =C2=A0 other_eal_param=3D'--single-file-segments'
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._lcore_list =3D f"-l {lcore_lis= t}"
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._memory_channels =3D f"-n {memo= ry_channels}"
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._prefix =3D prefix
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if prefix:
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._prefix =3D f"--f= ile-prefix=3D{prefix}"
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._no_pci =3D "--no-pci" if = no_pci else ""
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._vdevs =3D " ".join(f"= ;--vdev {vdev}" for vdev in vdevs)
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._other_eal_param =3D other_eal_param=
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 # if we just want a default shell, there = is no need to append the DPDK build
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 # directory to the path
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 default_path =3D shell_type.get_path() >
> -=C2=A0 =C2=A0 def __str__(self) -> str:
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 return (
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{self._lcore_list} &= quot;
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{self._memory_channe= ls} "
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{self._prefix} "= ;
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{self._no_pci} "= ;
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{self._vdevs} "=
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{self._other_eal_par= am}"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 if shell_type !=3D InteractiveApp.shell:<= br> > +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 default_path =3D self.main_= session.join_remote_path(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_d= pdk_build_dir, shell_type.get_path()
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return self.main_session.interactive_sess= ion.create_interactive_shell(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 shell_type,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 path_to_app if path_to_app = else default_path,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 str(eal_parameters) if eal_= parameters else "",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout,
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )

I forgot to mention that I'd like to change to structure a bit here - calling self.main_session.create_interactive_shell() would makes more
sense to me, as the interactive_session is basically an implementation
detail.

Sure, I'll make that change as wel= l.
=C2=A0

> diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_= smoke_tests.py
> new file mode 100644
> index 00000000..b7e70ee1
> --- /dev/null
> +++ b/dts/tests/TestSuite_smoke_tests.py
> @@ -0,0 +1,118 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2023 University of New Hampshire
> +
> +import re
> +
> +from framework.config import InteractiveApp
> +from framework.remote_session import TestPmdShell
> +from framework.settings import SETTINGS
> +from framework.test_suite import TestSuite
> +
> +
> +class SmokeTests(TestSuite):
> +=C2=A0 =C2=A0 is_blocking =3D True
> +=C2=A0 =C2=A0 # dicts in this list are expected to have two keys:
> +=C2=A0 =C2=A0 # "pci_address" and "current_driver"= ;
> +=C2=A0 =C2=A0 nics_in_node: list[dict[str, str]] =3D []
> +
> +=C2=A0 =C2=A0 def set_up_suite(self) -> None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Setup:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Set the build directory pat= h and generate a list of NICs in the SUT node.
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.dpdk_build_dir_path =3D self.sut_nod= e.remote_dpdk_build_dir
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 for nic in self.sut_node.config.ports: > +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 new_dict =3D {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "pci_add= ress": nic.pci,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "current= _driver": nic.os_driver.strip(),
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 }
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.nics_in_node.append(ne= w_dict)
> +
> +=C2=A0 =C2=A0 def test_unit_tests(self) -> None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Test:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Run the fast-test unit-test= suite through meson.
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sut_node.main_session.send_command(<= br> > +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"meson test -C {self.= dpdk_build_dir_path} --suite fast-tests",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 300,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 verify=3DTrue,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +
> +=C2=A0 =C2=A0 def test_driver_tests(self) -> None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Test:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Run the driver-test unit-te= st suite through meson.
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 list_of_vdevs =3D ""
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 for dev in self.sut_node._execution_confi= g.vdevs:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 list_of_vdevs +=3D f"-= -vdev {dev} "
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 list_of_vdevs =3D list_of_vdevs[:-1]
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 if list_of_vdevs:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.info(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "Running= driver tests with the following virtual "
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"device= s: {list_of_vdevs}"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sut_node.main_session.= send_command(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"meson = test -C {self.dpdk_build_dir_path} --suite driver-tests "
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f'--test-= args "{list_of_vdevs}"',
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 300,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 verify=3DTrue= ,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sut_node.main_session.= send_command(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"meson = test -C {self.dpdk_build_dir_path} --suite driver-tests",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 300,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 verify=3DTrue= ,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +
> +=C2=A0 =C2=A0 def test_devices_listed_in_testpmd(self) -> None: > +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Test:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Uses testpmd driver to veri= fy that devices have been found by testpmd.
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 testpmd_driver =3D self.sut_node.create_i= nteractive_shell(InteractiveApp.testpmd)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 # We know it should always be a TestPmdSh= ell but mypy doesn't
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 assert isinstance(testpmd_driver, TestPmd= Shell)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 dev_list: list[str] =3D testpmd_driver.ge= t_devices()
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 for nic in self.nics_in_node:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.verify(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 nic["pci= _address"] in dev_list,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"Device= {nic['pci_address']} was not listed in testpmd's available dev= ices, "
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "please = check your configuration",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +
> +=C2=A0 =C2=A0 def test_device_bound_to_driver(self) -> None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Test:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Ensure that all drivers lis= ted in the config are bound to the correct driver.
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 path_to_devbind =3D self.sut_node.main_se= ssion.join_remote_path(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sut_node._remote_dpdk_= dir, "usertools", "dpdk-devbind.py"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 regex_for_pci_address =3D "/[0-9]{4}= :[0-9]{2}:[0-9]{2}.[0-9]{1}/"

This shouldn't be tucked away in a test case. It should be in some
high-level module - let's put it to utils.py for the time being.

Good point, this could be something that is useful i= n many other places as well. I just created it there because that's jus= t where I needed it but it would be good to allow other people to reach it = easily.
=C2=A0

> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 all_nics_in_dpdk_devbind =3D self.sut_nod= e.main_session.send_command(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{path_to_devbind} --= status | awk '{regex_for_pci_address}'",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 SETTINGS.timeout,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout
> +
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 for nic in self.nics_in_node:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # This regular expression f= inds the line in the above string that starts
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # with the address for the = nic we are on in the loop and then captures the
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # name of the driver in a g= roup
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 devbind_info_for_nic =3D re= .search(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{nic[&= #39;pci_address']}[^\\n]*drv=3D([\\d\\w]*) [^\\n]*",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 all_nics_in_d= pdk_devbind,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.verify(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 devbind_info_= for_nic is not None,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"Failed= to find configured device ({nic['pci_address']}) using dpdk-devbin= d.py",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # We know this isn't No= ne, but mypy doesn't
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 assert devbind_info_for_nic= is not None
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.verify(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 devbind_info_= for_nic.group(1) =3D=3D nic["current_driver"],
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"Driver= for device {nic['pci_address']} does not match driver listed in &q= uot;
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"config= uration (bound to {devbind_info_for_nic.group(1)})",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
> --
> 2.41.0
>

Thank you for the comments!
Jeremy
--0000000000003319cb06003a4a96--