DPDK patches and discussions
 help / color / mirror / Atom feed
From: Jeremy Spewock <jspewock@iol.unh.edu>
To: "Juraj Linkeš" <juraj.linkes@pantheon.tech>
Cc: Honnappa.Nagarahalli@arm.com, thomas@monjalon.net,
	lijuan.tu@intel.com,  wathsala.vithanage@arm.com,
	probb@iol.unh.edu, dev@dpdk.org
Subject: Re: [PATCH v2 1/2] dts: add smoke tests
Date: Tue, 11 Jul 2023 14:30:01 -0400	[thread overview]
Message-ID: <CAAA20UR7HgBz4dr_95qzNiw1HHSCyEhYO+V-7fAu1p9-RT+Uzg@mail.gmail.com> (raw)
In-Reply-To: <CAOb5WZY97uyCX3MiT8eFo4XKJS14r2dHYTiNx=55+Y0xs+=v5w@mail.gmail.com>

[-- Attachment #1: Type: text/plain, Size: 69497 bytes --]

On Tue, Jul 11, 2023 at 5:42 AM Juraj Linkeš <juraj.linkes@pantheon.tech>
wrote:

> Just a few more comments.
>
> On Mon, Jul 10, 2023 at 6:23 PM <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
> > fail, the DTS execution terminates as part of a "fail-fast" model.
> >
> > Signed-off-by: Jeremy Spewock <jspewock@iol.unh.edu>
> > ---
> >  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.py
> >  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 execution
>
> 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=True, frozen=True)
> > +class PortConfig:
> > +    node: str
> > +    pci: str
> > +    os_driver_for_dpdk: str
> > +    os_driver: str
> > +    peer_node: str
> > +    peer_pci: str
> > +
> > +    @staticmethod
> > +    def from_dict(node: str, d: dict) -> "PortConfig":
> > +        return PortConfig(node=node, **d)
> > +
> > +
> >  @dataclass(slots=True, frozen=True)
> >  class NodeConfiguration:
> >      name: str
> > @@ -84,6 +99,7 @@ class NodeConfiguration:
> >      use_first_core: bool
> >      memory_channels: int
> >      hugepages: HugepageConfiguration | None
> > +    ports: list[PortConfig]
> >
> >      @staticmethod
> >      def from_dict(d: dict) -> "NodeConfiguration":
> > @@ -92,18 +108,43 @@ def from_dict(d: dict) -> "NodeConfiguration":
> >              if "force_first_numa" not in hugepage_config:
> >                  hugepage_config["force_first_numa"] = False
> >              hugepage_config = HugepageConfiguration(**hugepage_config)
> > +        common_config = {
> > +            "name": d["name"],
> > +            "hostname": d["hostname"],
> > +            "user": d["user"],
> > +            "password": d.get("password"),
> > +            "arch": Architecture(d["arch"]),
> > +            "os": OS(d["os"]),
> > +            "lcores": d.get("lcores", "1"),
> > +            "use_first_core": d.get("use_first_core", False),
> > +            "memory_channels": d.get("memory_channels", 1),
> > +            "hugepages": hugepage_config,
> > +            "ports": [PortConfig.from_dict(d["name"], port) for port in
> d["ports"]],
> > +        }
> > +
> > +        return NodeConfiguration(**common_config)
> > +
> > +
> > +@dataclass(slots=True)
>
> 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 generated
> at the start.
> > +    This is because we need to initialize a connection with the node
> before we can
> > +    collect the information needed in this class. Therefore, it cannot
> be a part of
> > +    the configuration class above.
> > +    """
> >
> > -        return NodeConfiguration(
> > -            name=d["name"],
> > -            hostname=d["hostname"],
> > -            user=d["user"],
> > -            password=d.get("password"),
> > -            arch=Architecture(d["arch"]),
> > -            os=OS(d["os"]),
> > -            lcores=d.get("lcores", "1"),
> > -            use_first_core=d.get("use_first_core", False),
> > -            memory_channels=d.get("memory_channels", 1),
> > -            hugepages=hugepage_config,
> > +    os_name: str
> > +    os_version: str
> > +    kernel_version: str
> > +
> > +    @staticmethod
> > +    def from_dict(d: dict):
> > +        return NodeInfo(
> > +            os_name=d["os_name"],
> > +            os_version=d["os_version"],
> > +            kernel_version=d["kernel_version"],
> >          )
>
> We don't need the from_dict method, as we can instantiate this class
> right away (node_info = NodeInfo(os_name=d["os_name"],
> os_version=d["os_version"], kernel_version=d["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=True)
>
> 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=d["dpdk_version"],
> compiler_version=d["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] = list(
> >              map(TestSuiteConfig.from_dict, d["test_suites"])
> >          )
> > -        sut_name = d["system_under_test"]
> > +        sut_name = d["system_under_test"]["node_name"]
> > +        skip_smoke_tests = d.get("skip_smoke_tests", False)
> >          assert sut_name in node_map, f"Unknown SUT {sut_name} in
> execution {d}"
> > -
> > +        vdevs = (
> > +            d["system_under_test"]["vdevs"] if "vdevs" in
> d["system_under_test"] else []
> > +        )
> >          return ExecutionConfiguration(
> >              build_targets=build_targets,
> >              perf=d["perf"],
> >              func=d["func"],
> > +            skip_smoke_tests=skip_smoke_tests,
> >              test_suites=test_suites,
> >              system_under_test=node_map[sut_name],
> > +            vdevs=vdevs,
> >          )
> >
> >
> > @@ -221,3 +287,27 @@ def load_config() -> Configuration:
> >
> >
> >  CONFIGURATION = load_config()
> > +
> > +
> > +@unique
> > +class InteractiveApp(Enum):
> > +    """An enum that represents different supported interactive
> applications
> > +
> > +    The values in this enum must all be set to objects that have a key
> called
> > +    "default_path" where "default_path" represents a PurPath object for
> the path
> > +    to the application. This default path will be passed into the
> handler class
> > +    for the application so that it can start the application. For every
> 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 = {"default_path": PurePath()}
> > +    testpmd = {"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 but
> greatly simplifies implementation. If there are an inconsistencies, then
> DTS will not run until that issue is fixed. An example inconsistency would
> be port 1, node 1 says it is connected to port 1, node 2, but port 1, node
> 2 says it is connected to port 2, node 1.",
> > +              "properties": {
> > +                "pci": {
> > +                  "$ref": "#/definitions/pci_address",
> > +                  "description": "The local PCI address of the port"
> > +                },
> > +                "os_driver_for_dpdk": {
> > +                  "type": "string",
> > +                  "description": "The driver that the kernel should
> bind this device to for DPDK to use it. (ex: vfio-pci)"
> > +                },
> > +                "os_driver": {
> > +                  "type": "string",
> > +                  "description": "The driver normally used by this port
> (ex: i40e)"
> > +                },
> > +                "peer_node": {
> > +                  "type": "string",
> > +                  "description": "The name of the node the peer port is
> on"
> > +                },
> > +                "peer_pci": {
> > +                  "$ref": "#/definitions/pci_address",
> > +                  "description": "The PCI address of the peer port"
> > +                }
> > +              },
> > +              "additionalProperties": false,
> > +              "required": [
> > +                "pci",
> > +                "os_driver_for_dpdk",
> > +                "os_driver",
> > +                "peer_node",
> > +                "peer_pci"
> > +              ]
> > +            },
> > +            "minimum": 1
> >            }
> >          },
> >          "additionalProperties": false,
> > @@ -211,8 +332,27 @@
> >                ]
> >              }
> >            },
> > +          "skip_smoke_tests": {
> > +            "description": "Optional field that allows you to skip
> smoke testing",
> > +            "type": "boolean"
> > +          },
> >            "system_under_test": {
> > -            "$ref": "#/definitions/node_name"
> > +            "type":"object",
> > +            "properties": {
> > +              "node_name": {
> > +                "$ref": "#/definitions/node_name"
> > +              },
> > +              "vdevs": {
> > +                "description": "Opentional list of names of vdevs to be
> used in execution",
> > +                "type": "array",
> > +                "items": {
> > +                  "type": "string"
> > +                }
> > +              }
> > +            },
> > +            "required": [
> > +              "node_name"
> > +            ]
> >            }
> >          },
> >          "additionalProperties": false,
> > diff --git a/dts/framework/dts.py b/dts/framework/dts.py
> > index 05022845..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 = result.add_execution(sut_node.config)
> > +    execution_result = result.add_execution(sut_node.config,
> sut_node.node_info)
> >
> >      try:
> >          sut_node.set_up_execution(execution)
> > @@ -118,14 +124,15 @@ def _run_build_target(
> >
> >      try:
> >          sut_node.set_up_build_target(build_target)
> > -        result.dpdk_version = sut_node.dpdk_version
> > +        # result.dpdk_version = sut_node.dpdk_version
> > +
> build_target_result.add_build_target_versions(sut_node.get_build_target_info())
> >          build_target_result.update_setup(Result.PASS)
> >      except Exception as e:
> >          dts_logger.exception("Build target setup failed.")
> >          build_target_result.update_setup(Result.FAIL, e)
> >
> >      else:
> > -        _run_suites(sut_node, execution, build_target_result)
> > +        _run_all_suites(sut_node, execution, build_target_result)
> >
> >      finally:
> >          try:
> > @@ -136,7 +143,7 @@ def _run_build_target(
> >              build_target_result.update_teardown(Result.FAIL, e)
> >
> >
> > -def _run_suites(
> > +def _run_all_suites(
> >      sut_node: SutNode,
> >      execution: ExecutionConfiguration,
> >      build_target_result: BuildTargetResult,
> > @@ -146,27 +153,62 @@ def _run_suites(
> >      with possibly only a subset of test cases.
> >      If no subset is specified, run all test cases.
> >      """
> > +    end_build_target = False
> > +    if not execution.skip_smoke_tests:
> > +        execution.test_suites[:0] =
> [TestSuiteConfig.from_dict("smoke_tests")]
> >      for test_suite_config in execution.test_suites:
> >          try:
> > -            full_suite_path =
> f"tests.TestSuite_{test_suite_config.test_suite}"
> > -            test_suite_classes = get_test_suites(full_suite_path)
> > -            suites_str = ", ".join((x.__name__ for x in
> test_suite_classes))
> > -            dts_logger.debug(
> > -                f"Found test suites '{suites_str}' in
> '{full_suite_path}'."
> > +            _run_single_suite(
> > +                sut_node, execution, build_target_result,
> test_suite_config
> >              )
> > -        except Exception as e:
> > -            dts_logger.exception("An error occurred when searching for
> test suites.")
> > -            result.update_setup(Result.ERROR, e)
> > -
> > -        else:
> > -            for test_suite_class in test_suite_classes:
> > -                test_suite = test_suite_class(
> > -                    sut_node,
> > -                    test_suite_config.test_cases,
> > -                    execution.func,
> > -                    build_target_result,
> > -                )
> > -                test_suite.run()
> > +        except BlockingTestSuiteError as e:
> > +            dts_logger.exception(
> > +                f"An error occurred within
> {test_suite_config.test_suite}. "
> > +                "Skipping build target..."
> > +            )
> > +            result.add_error(e)
> > +            end_build_target = True
> > +        # if a blocking test failed and we need to bail out of suite
> executions
> > +        if end_build_target:
> > +            break
> > +
> > +
> > +def _run_single_suite(
> > +    sut_node: SutNode,
> > +    execution: ExecutionConfiguration,
> > +    build_target_result: BuildTargetResult,
> > +    test_suite_config: TestSuiteConfig,
> > +) -> None:
> > +    """Runs a single test suite.
> > +
> > +    Args:
> > +        sut_node: Node to run tests on.
> > +        execution: Execution the test case belongs to.
> > +        build_target_result: Build target configuration test case is
> run on
> > +        test_suite_config: Test suite configuration
> > +
> > +    Raises:
> > +        BlockingTestSuiteError: If a test suite that was marked as
> blocking fails.
> > +    """
> > +    try:
> > +        full_suite_path =
> f"tests.TestSuite_{test_suite_config.test_suite}"
> > +        test_suite_classes = get_test_suites(full_suite_path)
> > +        suites_str = ", ".join((x.__name__ for x in test_suite_classes))
> > +        dts_logger.debug(f"Found test suites '{suites_str}' in
> '{full_suite_path}'.")
> > +    except Exception as e:
> > +        dts_logger.exception("An error occurred when searching for test
> suites.")
> > +        result.update_setup(Result.ERROR, e)
> > +
> > +    else:
> > +        for test_suite_class in test_suite_classes:
> > +            test_suite = test_suite_class(
> > +                sut_node,
> > +                test_suite_config.test_cases,
> > +                execution.func,
> > +                build_target_result,
> > +                result,
> > +            )
> > +            test_suite.run()
> >
> >
> >  def _exit_dts() -> None:
> > diff --git a/dts/framework/exception.py b/dts/framework/exception.py
> > index ca353d98..dfb12df4 100644
> > --- a/dts/framework/exception.py
> > +++ b/dts/framework/exception.py
> > @@ -25,6 +25,7 @@ class ErrorSeverity(IntEnum):
> >      SSH_ERR = 4
> >      DPDK_BUILD_ERR = 10
> >      TESTCASE_VERIFY_ERR = 20
> > +    BLOCKING_TESTSUITE_ERR = 25
> >
> >
> >  class DTSError(Exception):
> > @@ -144,3 +145,14 @@ def __init__(self, value: str):
> >
> >      def __str__(self) -> str:
> >          return repr(self.value)
> > +
> > +
> > +class BlockingTestSuiteError(DTSError):
> > +    suite_name: str
> > +    severity: ClassVar[ErrorSeverity] =
> ErrorSeverity.BLOCKING_TESTSUITE_ERR
> > +
> > +    def __init__(self, suite_name: str) -> None:
> > +        self.suite_name = suite_name
> > +
> > +    def __str__(self) -> str:
> > +        return f"Blocking suite {self.suite_name} failed."
> > diff --git a/dts/framework/remote_session/__init__.py
> b/dts/framework/remote_session/__init__.py
> > index ee221503..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_session
> > +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 = name
> >          self._logger = logger
> >          self.remote_session = create_remote_session(node_config, name,
> logger)
> > +        self.interactive_session =
> create_interactive_session(node_config, name, logger)
> >
> >      def close(self, force: bool = False) -> None:
> >          """
> > @@ -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 = self.send_command(
> > +            "awk -F= '$1 ~ /^NAME$|^VERSION$/ {print $2}'
> /etc/os-release",
> > +            SETTINGS.timeout,
> > +        ).stdout.split("\n")
> > +        kernel_version = self.send_command("uname -r",
> SETTINGS.timeout).stdout
> > +        return NodeInfo(
> > +            os_release_info[0].strip(), os_release_info[1].strip(),
> kernel_version
> > +        )
> > diff --git a/dts/framework/remote_session/remote/__init__.py
> b/dts/framework/remote_session/remote/__init__.py
> > index 8a151221..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=W0611
> >
> >  from framework.config import NodeConfiguration
> >  from framework.logger import DTSLOG
> >
> > +from .interactive_remote_session import InteractiveRemoteSession
> > +from .interactive_shell import InteractiveShell
> >  from .remote_session import CommandResult, RemoteSession
> >  from .ssh_session import SSHSession
> > +from .testpmd_shell import 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: ignore
> > +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 = node_config
> > +        self._logger = _logger
> > +        self.hostname = node_config.hostname
> > +        self.username = node_config.user
> > +        self.password = node_config.password if node_config.password
> else ""
> > +        port = "22"
> > +        self.ip = node_config.hostname
> > +        if ":" in node_config.hostname:
> > +            self.ip, port = node_config.hostname.split(":")
> > +        self.port = int(port)
> > +        self._logger.info(
> > +            f"Initializing interactive connection for {self.username}@
> {self.hostname}"
> > +        )
> > +        self._connect()
> > +        self._logger.info(
> > +            f"Interactive connection successful for {self.username}@
> {self.hostname}"
> > +        )
> > +
> > +    def _connect(self) -> None:
> > +        client = SSHClient()
> > +        client.set_missing_host_key_policy(AutoAddPolicy)
> > +        self.session = client
> > +        retry_attempts = 10
> > +        for retry_attempt in range(retry_attempts):
> > +            try:
> > +                client.connect(
> > +                    self.ip,
> > +                    username=self.username,
> > +                    port=self.port,
> > +                    password=self.password,
> > +                    timeout=20 if self.port else 10,
> > +                )
> > +            except (TypeError, BadHostKeyException,
> AuthenticationException) as e:
> > +                self._logger.exception(e)
> > +                raise SSHConnectionError(self.hostname) from e
> > +            except (NoValidConnectionsError, socket.error,
> SSHException) as e:
> > +                self._logger.debug(traceback.format_exc())
> > +                self._logger.warning(e)
> > +                self._logger.info(
> > +                    "Retrying interactive session connection: "
> > +                    f"retry number {retry_attempt +1}"
> > +                )
> > +            else:
> > +                break
> > +        else:
> > +            raise SSHConnectionError(self.hostname)
> > +        # Interactive sessions are used on an "as needed" basis so we
> have
> > +        # to set a keepalive
> > +        self._transport = self.session.get_transport()
> > +        if self._transport is not None:
> > +            self._transport.set_keepalive(30)
> > +
> > +    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=timeout,
> > +                    eal_flags=eal_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 = SETTINGS.timeout,
> > +    ) -> None:
> > +        self._interactive_session = interactive_session
> > +        self._ssh_channel = self._interactive_session.invoke_shell()
> > +        self._stdin = self._ssh_channel.makefile_stdin("w")
> > +        self._stdout = self._ssh_channel.makefile("r")
> > +        self._ssh_channel.settimeout(timeout)
> > +        self._ssh_channel.set_combine_stderr(True)  # combines stdout
> and stderr streams
> > +        self._logger = logger
> > +        self._timeout = timeout
> > +        self._path_to_app = path_to_app
> > +
> > +
> > +    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 simply
> 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) -> str:
> > +        """Send a command and get all output before the expected ending
> string.
> > +
> > +        Lines that expect input are not included in the stdout buffer
> so they cannot be
> > +        used for expect. For example, if you were prompted to log into
> something
> > +        with a username and password, you cannot expect "username:"
> because it won't
> > +        yet be in the stdout buffer. A work around for this could be
> consuming an
> > +        extra newline character to force the current prompt into the
> stdout buffer.
> > +
> > +        Returns:
> > +            All output in the buffer before expected string
> > +        """
> > +        self._logger.info(f"Sending command {command.strip()}...")
> > +        self._stdin.write(f"{command}\n")
> > +        self._stdin.flush()
> > +        out: str = ""
> > +        for line in self._stdout:
> > +            out += line
> > +            if prompt in line and not line.rstrip().endswith(
> > +                command.rstrip()
> > +            ):  # ignore line that sent command
> > +                break
> > +        self._logger.debug(f"Got output: {out}")
> > +        return out
> > +
> > +    def close(self) -> None:
> > +        self._stdin.close()
> > +        self._ssh_channel.close()
> > +
> > +    def __del__(self) -> None:
> > +        self.close()
> > diff --git a/dts/framework/remote_session/remote/testpmd_shell.py
> b/dts/framework/remote_session/remote/testpmd_shell.py
> > new file mode 100644
> > index 00000000..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 = "testpmd>"
> > +    _eal_flags: str
> > +
> > +    def __init__(
> > +        self,
> > +        interactive_session: SSHClient,
> > +        logger: DTSLOG,
> > +        path_to_testpmd: PurePath,
> > +        eal_flags: str,
> > +        timeout: float = SETTINGS.timeout,
> > +    ) -> None:
> > +        """Initializes an interactive testpmd session using specified
> parameters."""
> > +        self._eal_flags = eal_flags
> > +
> > +        super(TestPmdShell, self).__init__(
> > +            interactive_session,
> > +            logger=logger,
> > +            path_to_app=path_to_testpmd,
> > +            timeout=timeout,
> > +        )
> > +        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 = expected_prompt)
> -> str:
> > +        """Specific way of handling the command for testpmd
> > +
> > +        An extra newline character is consumed in order to force the
> current line into
> > +        the stdout buffer.
> > +        """
> > +        return self.send_command_get_output(f"{command}\n", prompt)
> > +
> > +    def get_devices(self) -> list[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 = self.send_command("show device info all")
> > +        dev_list: list[str] = []
> > +        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.py
> > 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] = 0
> >          self["PASS RATE"] = 0.0
> > -        self["DPDK VERSION"] = dpdk_version
> > +        if output_info:
> > +            for info_key, info_val in output_info.items():
> > +                self[info_key] = info_val
> >
> >      def __iadd__(self, other: Result) -> "Statistics":
> >          """
> > @@ -206,6 +211,8 @@ class BuildTargetResult(BaseResult):
> >      os: OS
> >      cpu: CPUType
> >      compiler: Compiler
> > +    compiler_version: str | None
> > +    dpdk_version: str | None
> >
> >      def __init__(self, build_target: BuildTargetConfiguration):
> >          super(BuildTargetResult, self).__init__()
> > @@ -213,6 +220,12 @@ def __init__(self, build_target:
> BuildTargetConfiguration):
> >          self.os = build_target.os
> >          self.cpu = build_target.cpu
> >          self.compiler = build_target.compiler
> > +        self.compiler_version = None
> > +        self.dpdk_version = None
> > +
> > +    def add_build_target_versions(self, versions: BuildTargetInfo) ->
> None:
> > +        self.compiler_version = versions.compiler_version
> > +        self.dpdk_version = versions.dpdk_version
> >
> >      def add_test_suite(self, test_suite_name: str) -> TestSuiteResult:
> >          test_suite_result = TestSuiteResult(test_suite_name)
> > @@ -228,10 +241,17 @@ class ExecutionResult(BaseResult):
> >      """
> >
> >      sut_node: NodeConfiguration
> > +    sut_os_name: str
> > +    sut_os_version: str
> > +    sut_kernel_version: str
> >
> > -    def __init__(self, sut_node: NodeConfiguration):
> > +    def __init__(self, sut_node: NodeConfiguration, sut_version_info:
> NodeInfo):
> >          super(ExecutionResult, self).__init__()
> >          self.sut_node = sut_node
> > +        self.sut_version_info = sut_version_info
> > +        self.sut_os_name = sut_version_info.os_name
> > +        self.sut_os_version = sut_version_info.os_version
> > +        self.sut_kernel_version = sut_version_info.kernel_version
> >
> >      def add_build_target(
> >          self, build_target: BuildTargetConfiguration
> > @@ -258,6 +278,7 @@ class DTSResult(BaseResult):
> >      """
> >
> >      dpdk_version: str | None
> > +    output: dict | None
> >      _logger: DTSLOG
> >      _errors: list[Exception]
> >      _return_code: ErrorSeverity
> > @@ -267,14 +288,17 @@ class DTSResult(BaseResult):
> >      def __init__(self, logger: DTSLOG):
> >          super(DTSResult, self).__init__()
> >          self.dpdk_version = None
> > +        self.output = None
> >          self._logger = logger
> >          self._errors = []
> >          self._return_code = ErrorSeverity.NO_ERR
> >          self._stats_result = None
> >          self._stats_filename = os.path.join(SETTINGS.output_dir,
> "statistics.txt")
> >
> > -    def add_execution(self, sut_node: NodeConfiguration) ->
> ExecutionResult:
> > -        execution_result = ExecutionResult(sut_node)
> > +    def add_execution(
> > +        self, sut_node: NodeConfiguration, sut_version_info: NodeInfo
> > +    ) -> ExecutionResult:
> > +        execution_result = ExecutionResult(sut_node, sut_version_info)
> >          self._inner_results.append(execution_result)
> >          return execution_result
> >
> > @@ -296,7 +320,8 @@ def process(self) -> None:
> >              for error in self._errors:
> >                  self._logger.debug(repr(error))
> >
> > -        self._stats_result = Statistics(self.dpdk_version)
> > +        self._stats_result = Statistics(self.output)
> > +        # add information gathered from the smoke tests to the
> statistics
> >          self.add_stats(self._stats_result)
> >          with open(self._stats_filename, "w+") as stats_file:
> >              stats_file.write(str(self._stats_result))
> > diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py
> > index 0705f38f..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 = 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 = sut_node
> >          self._logger = getLogger(self.__class__.__name__)
> > @@ -55,6 +69,7 @@ def __init__(
> >          self._test_cases_to_run.extend(SETTINGS.test_cases)
> >          self._func = func
> >          self._result =
> build_target_result.add_test_suite(self.__class__.__name__)
> > +        self._dts_result = 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 = node_config
> > @@ -64,6 +65,7 @@ def set_up_execution(self, execution_config:
> ExecutionConfiguration) -> None:
> >          """
> >          self._setup_hugepages()
> >          self._set_up_execution(execution_config)
> > +        self._execution_config = execution_config
> >
> >      def _set_up_execution(self, execution_config:
> ExecutionConfiguration) -> None:
> >          """
> > diff --git a/dts/framework/testbed_model/sut_node.py
> b/dts/framework/testbed_model/sut_node.py
> > index 2b2b50d9..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, NodeConfiguration
> > -from framework.remote_session import CommandResult, OSSession
> > +from typing import Union
> > +
> > +from framework.config import (
> > +    BuildTargetConfiguration,
> > +    BuildTargetInfo,
> > +    InteractiveApp,
> > +    NodeConfiguration,
> > +    NodeInfo,
> > +)
> > +from framework.remote_session import (
> > +    CommandResult,
> > +    InteractiveShell,
> > +    OSSession,
> > +    TestPmdShell,
> > +)
> >  from framework.settings import SETTINGS
> >  from framework.utils import EnvVarsDict, MesonArgs
> >
> > @@ -16,6 +29,52 @@
> >  from .node import Node
> >
> >
> > +class EalParameters(object):
> > +    def __init__(
> > +        self,
> > +        lcore_list: LogicalCoreList,
> > +        memory_channels: int,
> > +        prefix: str,
> > +        no_pci: bool,
> > +        vdevs: list[VirtualDevice],
> > +        other_eal_param: str,
> > +    ):
> > +        """
> > +        Generate eal parameters character string;
> > +        :param lcore_list: the list of logical cores to use.
> > +        :param memory_channels: the number of memory channels to use.
> > +        :param prefix: set file prefix string, eg:
> > +                        prefix='vf'
> > +        :param no_pci: switch of disable PCI bus eg:
> > +                        no_pci=True
> > +        :param vdevs: virtual device list, eg:
> > +                        vdevs=[
> > +                            VirtualDevice('net_ring0'),
> > +                            VirtualDevice('net_ring1')
> > +                        ]
> > +        :param other_eal_param: user defined DPDK eal parameters, eg:
> > +                        other_eal_param='--single-file-segments'
> > +        """
> > +        self._lcore_list = f"-l {lcore_list}"
> > +        self._memory_channels = f"-n {memory_channels}"
> > +        self._prefix = prefix
> > +        if prefix:
> > +            self._prefix = f"--file-prefix={prefix}"
> > +        self._no_pci = "--no-pci" if no_pci else ""
> > +        self._vdevs = " ".join(f"--vdev {vdev}" for vdev in vdevs)
> > +        self._other_eal_param = other_eal_param
> > +
> > +    def __str__(self) -> str:
> > +        return (
> > +            f"{self._lcore_list} "
> > +            f"{self._memory_channels} "
> > +            f"{self._prefix} "
> > +            f"{self._no_pci} "
> > +            f"{self._vdevs} "
> > +            f"{self._other_eal_param}"
> > +        )
> > +
> > +
> >  class SutNode(Node):
> >      """
> >      A class for managing connections to the System under Test, providing
> > @@ -30,9 +89,11 @@ class SutNode(Node):
> >      _env_vars: EnvVarsDict
> >      _remote_tmp_dir: PurePath
> >      __remote_dpdk_dir: PurePath | None
> > -    _dpdk_version: str | None
> >      _app_compile_timeout: float
> >      _dpdk_kill_session: OSSession | None
> > +    _dpdk_version: str | None
> > +    _node_info: NodeInfo | None
> > +    _compiler_version: str | None
> >
> >      def __init__(self, node_config: NodeConfiguration):
> >          super(SutNode, self).__init__(node_config)
> > @@ -41,12 +102,14 @@ def __init__(self, node_config: NodeConfiguration):
> >          self._env_vars = EnvVarsDict()
> >          self._remote_tmp_dir = self.main_session.get_remote_tmp_dir()
> >          self.__remote_dpdk_dir = None
> > -        self._dpdk_version = None
> >          self._app_compile_timeout = 90
> >          self._dpdk_kill_session = None
> >          self._dpdk_timestamp = (
> >              f"{str(os.getpid())}_{time.strftime('%Y%m%d%H%M%S',
> time.localtime())}"
> >          )
> > +        self._dpdk_version = None
> > +        self._node_info = None
> > +        self._compiler_version = None
> >
> >      @property
> >      def _remote_dpdk_dir(self) -> PurePath:
> > @@ -75,6 +138,32 @@ def dpdk_version(self) -> str:
> >              )
> >          return self._dpdk_version
> >
> > +    @property
> > +    def node_info(self) -> NodeInfo:
> > +        if self._node_info is None:
> > +            self._node_info = self.main_session.get_node_info()
> > +        return self._node_info
> > +
> > +    @property
> > +    def compiler_version(self) -> str:
> > +        if self._compiler_version is None:
> > +            if self._build_target_config is not None:
> > +                self._compiler_version =
> self.main_session.get_compiler_version(
> > +                    self._build_target_config.compiler.name
> > +                )
> > +            else:
> > +                self._logger.warning(
> > +                    "Failed to get compiler version because"
> > +                    "_build_target_config is None."
> > +                )
> > +                return ""
> > +        return self._compiler_version
> > +
> > +    def get_build_target_info(self) -> BuildTargetInfo:
> > +        return BuildTargetInfo(
> > +            dpdk_version=self.dpdk_version,
> compiler_version=self.compiler_version
> > +        )
> > +
> >      def _guess_dpdk_remote_dir(self) -> PurePath:
> >          return
> self.main_session.guess_dpdk_remote_dir(self._remote_tmp_dir)
> >
> > @@ -84,6 +173,10 @@ def _set_up_build_target(
> >          """
> >          Setup DPDK on the SUT node.
> >          """
> > +        # we want to ensure that dpdk_version and compiler_version is
> reset for new
> > +        # build targets
> > +        self._dpdk_version = None
> > +        self._compiler_version = None
> >          self._configure_build_target(build_target_config)
> >          self._copy_dpdk_tarball()
> >          self._build_dpdk()
> > @@ -262,48 +355,43 @@ def run_dpdk_app(
> >              f"{app_path} {eal_args}", timeout, verify=True
> >          )
> >
> > -
> > -class EalParameters(object):
> > -    def __init__(
> > +    def create_interactive_shell(
> >          self,
> > -        lcore_list: LogicalCoreList,
> > -        memory_channels: int,
> > -        prefix: str,
> > -        no_pci: bool,
> > -        vdevs: list[VirtualDevice],
> > -        other_eal_param: str,
> > -    ):
> > +        shell_type: InteractiveApp,
> > +        path_to_app: PurePath | None = None,
> > +        timeout: float = SETTINGS.timeout,
> > +        eal_parameters: EalParameters | None = None,
> > +    ) -> Union[InteractiveShell, TestPmdShell]:
> > +        """Create a handler for an interactive session.
> > +
> > +        This method is a factory that calls a method in OSSession to
> create shells for
> > +        different DPDK applications.
> > +
> > +        Args:
> > +            shell_type: Enum value representing the desired application.
> > +            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 the
> 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='vf'
> > -        :param no_pci: switch of disable PCI bus eg:
> > -                        no_pci=True
> > -        :param vdevs: virtual device list, eg:
> > -                        vdevs=[
> > -                            VirtualDevice('net_ring0'),
> > -                            VirtualDevice('net_ring1')
> > -                        ]
> > -        :param other_eal_param: user defined DPDK eal parameters, eg:
> > -                        other_eal_param='--single-file-segments'
> > -        """
> > -        self._lcore_list = f"-l {lcore_list}"
> > -        self._memory_channels = f"-n {memory_channels}"
> > -        self._prefix = prefix
> > -        if prefix:
> > -            self._prefix = f"--file-prefix={prefix}"
> > -        self._no_pci = "--no-pci" if no_pci else ""
> > -        self._vdevs = " ".join(f"--vdev {vdev}" for vdev in vdevs)
> > -        self._other_eal_param = other_eal_param
> > +        # if we just want a default shell, there is no need to append
> the DPDK build
> > +        # directory to the path
> > +        default_path = 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 != InteractiveApp.shell:
> > +            default_path = 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 = True
> > +    # dicts in this list are expected to have two keys:
> > +    # "pci_address" and "current_driver"
> > +    nics_in_node: list[dict[str, str]] = []
> > +
> > +    def set_up_suite(self) -> None:
> > +        """
> > +        Setup:
> > +            Set the build directory path and generate a list of NICs in
> the SUT node.
> > +        """
> > +        self.dpdk_build_dir_path = self.sut_node.remote_dpdk_build_dir
> > +        for nic in self.sut_node.config.ports:
> > +            new_dict = {
> > +                "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=True,
> > +        )
> > +
> > +    def test_driver_tests(self) -> None:
> > +        """
> > +        Test:
> > +            Run the driver-test unit-test suite through meson.
> > +        """
> > +        list_of_vdevs = ""
> > +        for dev in self.sut_node._execution_config.vdevs:
> > +            list_of_vdevs += f"--vdev {dev} "
> > +        list_of_vdevs = list_of_vdevs[:-1]
> > +        if list_of_vdevs:
> > +            self._logger.info(
> > +                "Running driver tests with the following virtual "
> > +                f"devices: {list_of_vdevs}"
> > +            )
> > +            self.sut_node.main_session.send_command(
> > +                f"meson test -C {self.dpdk_build_dir_path} --suite
> driver-tests "
> > +                f'--test-args "{list_of_vdevs}"',
> > +                300,
> > +                verify=True,
> > +            )
> > +        else:
> > +            self.sut_node.main_session.send_command(
> > +                f"meson test -C {self.dpdk_build_dir_path} --suite
> driver-tests",
> > +                300,
> > +                verify=True,
> > +            )
> > +
> > +    def test_devices_listed_in_testpmd(self) -> None:
> > +        """
> > +        Test:
> > +            Uses testpmd driver to verify that devices have been found
> by testpmd.
> > +        """
> > +        testpmd_driver =
> self.sut_node.create_interactive_shell(InteractiveApp.testpmd)
> > +        # We know it should always be a TestPmdShell but mypy doesn't
> > +        assert isinstance(testpmd_driver, TestPmdShell)
> > +        dev_list: list[str] = 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 = self.sut_node.main_session.join_remote_path(
> > +            self.sut_node._remote_dpdk_dir, "usertools",
> "dpdk-devbind.py"
> > +        )
> > +
> > +        regex_for_pci_address = "/[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 =
> self.sut_node.main_session.send_command(
> > +            f"{path_to_devbind} --status | awk
> '{regex_for_pci_address}'",
> > +            SETTINGS.timeout,
> > +        ).stdout
> > +
> > +        for nic in self.nics_in_node:
> > +            # This regular expression finds the line in the above
> string that starts
> > +            # with the address for the nic we are on in the loop and
> then captures the
> > +            # name of the driver in a group
> > +            devbind_info_for_nic = re.search(
> > +                f"{nic['pci_address']}[^\\n]*drv=([\\d\\w]*) [^\\n]*",
> > +                all_nics_in_dpdk_devbind,
> > +            )
> > +            self.verify(
> > +                devbind_info_for_nic is not None,
> > +                f"Failed to find configured device
> ({nic['pci_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) == nic["current_driver"],
> > +                f"Driver for device {nic['pci_address']} does not match
> driver listed in "
> > +                f"configuration (bound to
> {devbind_info_for_nic.group(1)})",
> > +            )
> > --
> > 2.41.0
> >
>

Thank you for the comments!
Jeremy

[-- Attachment #2: Type: text/html, Size: 91376 bytes --]

  reply	other threads:[~2023-07-11 18:30 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-07-10 16:19 [PATCH v2 0/2] Add DTS " jspewock
2023-07-10 16:19 ` [PATCH v2 1/2] dts: add " jspewock
2023-07-11  9:42   ` Juraj Linkeš
2023-07-11 18:30     ` Jeremy Spewock [this message]
2023-07-10 16:19 ` [PATCH v2 2/2] dts: add paramiko to dependencies jspewock
2023-07-10 22:09   ` Patrick Robb
2023-08-18 19:44   ` Adam Hassick
2023-08-18 18:34 ` [PATCH v2 0/2] Add DTS smoke tests Adam Hassick

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=CAAA20UR7HgBz4dr_95qzNiw1HHSCyEhYO+V-7fAu1p9-RT+Uzg@mail.gmail.com \
    --to=jspewock@iol.unh.edu \
    --cc=Honnappa.Nagarahalli@arm.com \
    --cc=dev@dpdk.org \
    --cc=juraj.linkes@pantheon.tech \
    --cc=lijuan.tu@intel.com \
    --cc=probb@iol.unh.edu \
    --cc=thomas@monjalon.net \
    --cc=wathsala.vithanage@arm.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).