get:
Show a patch.

patch:
Update a patch.

put:
Update a patch.

GET /api/patches/126841/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 126841,
    "url": "http://patchwork.dpdk.org/api/patches/126841/?format=api",
    "web_url": "http://patchwork.dpdk.org/project/dpdk/patch/20230512192540.401-3-jspewock@iol.unh.edu/",
    "project": {
        "id": 1,
        "url": "http://patchwork.dpdk.org/api/projects/1/?format=api",
        "name": "DPDK",
        "link_name": "dpdk",
        "list_id": "dev.dpdk.org",
        "list_email": "dev@dpdk.org",
        "web_url": "http://core.dpdk.org",
        "scm_url": "git://dpdk.org/dpdk",
        "webscm_url": "http://git.dpdk.org/dpdk",
        "list_archive_url": "https://inbox.dpdk.org/dev",
        "list_archive_url_format": "https://inbox.dpdk.org/dev/{}",
        "commit_url_format": ""
    },
    "msgid": "<20230512192540.401-3-jspewock@iol.unh.edu>",
    "list_archive_url": "https://inbox.dpdk.org/dev/20230512192540.401-3-jspewock@iol.unh.edu",
    "date": "2023-05-12T19:25:43",
    "name": "[RFC,v2,1/2] dts: add smoke tests",
    "commit_ref": null,
    "pull_url": null,
    "state": "superseded",
    "archived": true,
    "hash": "fc69a37099a65285189a464241a405295b1d737f",
    "submitter": {
        "id": 2772,
        "url": "http://patchwork.dpdk.org/api/people/2772/?format=api",
        "name": "Jeremy Spewock",
        "email": "jspewock@iol.unh.edu"
    },
    "delegate": {
        "id": 1,
        "url": "http://patchwork.dpdk.org/api/users/1/?format=api",
        "username": "tmonjalo",
        "first_name": "Thomas",
        "last_name": "Monjalon",
        "email": "thomas@monjalon.net"
    },
    "mbox": "http://patchwork.dpdk.org/project/dpdk/patch/20230512192540.401-3-jspewock@iol.unh.edu/mbox/",
    "series": [
        {
            "id": 27990,
            "url": "http://patchwork.dpdk.org/api/series/27990/?format=api",
            "web_url": "http://patchwork.dpdk.org/project/dpdk/list/?series=27990",
            "date": "2023-05-12T19:25:40",
            "name": "add DTS smoke tests",
            "version": 2,
            "mbox": "http://patchwork.dpdk.org/series/27990/mbox/"
        }
    ],
    "comments": "http://patchwork.dpdk.org/api/patches/126841/comments/",
    "check": "warning",
    "checks": "http://patchwork.dpdk.org/api/patches/126841/checks/",
    "tags": {},
    "related": [],
    "headers": {
        "Return-Path": "<dev-bounces@dpdk.org>",
        "X-Original-To": "patchwork@inbox.dpdk.org",
        "Delivered-To": "patchwork@inbox.dpdk.org",
        "Received": [
            "from mails.dpdk.org (mails.dpdk.org [217.70.189.124])\n\tby inbox.dpdk.org (Postfix) with ESMTP id 7C43642AE8;\n\tFri, 12 May 2023 21:27:56 +0200 (CEST)",
            "from mails.dpdk.org (localhost [127.0.0.1])\n\tby mails.dpdk.org (Postfix) with ESMTP id 6E843406BA;\n\tFri, 12 May 2023 21:27:56 +0200 (CEST)",
            "from mail-pl1-f226.google.com (mail-pl1-f226.google.com\n [209.85.214.226])\n by mails.dpdk.org (Postfix) with ESMTP id 32DDF406B3\n for <dev@dpdk.org>; Fri, 12 May 2023 21:27:55 +0200 (CEST)",
            "by mail-pl1-f226.google.com with SMTP id\n d9443c01a7336-1ab05018381so95667585ad.2\n for <dev@dpdk.org>; Fri, 12 May 2023 12:27:55 -0700 (PDT)",
            "from postal.iol.unh.edu (postal.iol.unh.edu. [132.177.123.84])\n by smtp-relay.gmail.com with ESMTPS id\n ju13-20020a170903428d00b001a6484fd026sm680934plb.115.2023.05.12.12.27.54\n (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128);\n Fri, 12 May 2023 12:27:54 -0700 (PDT)",
            "from iol.unh.edu (unknown\n [IPv6:2606:4100:3880:1271:90f9:1b64:f6e6:867f])\n by postal.iol.unh.edu (Postfix) with ESMTP id 72ADD605246B;\n Fri, 12 May 2023 15:27:53 -0400 (EDT)"
        ],
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=iol.unh.edu; s=unh-iol; t=1683919674; x=1686511674;\n h=content-transfer-encoding:mime-version:references:in-reply-to\n :message-id:date:subject:cc:to:from:from:to:cc:subject:date\n :message-id:reply-to;\n bh=eFi9qByGtlFnJiQN83k5R0gUjmkvYgFEcabbCTe2qB8=;\n b=evK+wgQ/PB1rMXTHfCyQp/fDPaxPK/mZx48IDTzg9hZR6QRLwzZSEpevwUv+HmTTTk\n SHRfcgIFOT3Uw7w1ck6KMzMvbu6652sNh+n94sjlj5ayC2VsCXhzdMG8N60HsxKuO4Wn\n Zpb2vqcRRoKQYQzJFff9gjIb+xjYOgq5K183Q=",
        "X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=1e100.net; s=20221208; t=1683919674; x=1686511674;\n h=content-transfer-encoding:mime-version:references:in-reply-to\n :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc\n :subject:date:message-id:reply-to;\n bh=eFi9qByGtlFnJiQN83k5R0gUjmkvYgFEcabbCTe2qB8=;\n b=YrRJAtUB2zxVO6fUTjfIV7cxCif6eJ7Tz7pT4hxVUQXyE08WB1c/dmSGNNidcq/riV\n 5FWL6lcXUdA3av+VtAy3vRDn5+Pv5ff+Kb2+/5RPAwRD8HFmb1OMHjo1CNsVy/1LFrF7\n 4LXBWBa1ic5xanvp/S4thboNrKVlXvH/eGdZXYzN1n15oix/ZT7zjbhdg0XAjJLEmkdi\n 6e0fFg3Ui3rKPUEHCeQrGt5cbfVsufIFqtVHrh9GZxChFNw9hR8rIH6ge9CUe6tf/nUH\n 2U6CYpJFxoPVyGjU9j+/Im9O6W+3a/ZHUFChiEzx3nX5x0LvVzRwV+98LDe/Vx7MpOQx\n Ga4Q==",
        "X-Gm-Message-State": "AC+VfDx0aNy+1OkHKIwhLBLlMlTyUl3vnqFxydwu23eRj2JhiSChis6m\n JNgnAG9Qo5ORBgZ/XvgMursJuHU4LeFUVrJUa5AldRrvnHq+ogYLYnEtPxPxoOs216CDhCI5e7k\n 6wRRO1Q9CrTIG9ScgMbsuednSe4pig8/oeOOcdNOcfdEkQxDUCRHFIE2kHhbAsIO9XmP1/omLXX\n IqD2cL0A==",
        "X-Google-Smtp-Source": "\n ACHHUZ5fPK/VH8ZLeYdYjvijUXeex19+reKLltAbZQP+pTaAkYbP2oHs21k33oFHpcjzeqiAZxv1pPJ+Jw5G",
        "X-Received": "by 2002:a17:903:22c7:b0:1a6:71b1:a0b9 with SMTP id\n y7-20020a17090322c700b001a671b1a0b9mr32345308plg.47.1683919674260;\n Fri, 12 May 2023 12:27:54 -0700 (PDT)",
        "X-Relaying-Domain": "iol.unh.edu",
        "From": "jspewock@iol.unh.edu",
        "To": "dev@dpdk.org",
        "Cc": "Jeremy Spewock <jspewock@iol.unh.edu>",
        "Subject": "[RFC v2 1/2] dts: add smoke tests",
        "Date": "Fri, 12 May 2023 15:25:43 -0400",
        "Message-Id": "<20230512192540.401-3-jspewock@iol.unh.edu>",
        "X-Mailer": "git-send-email 2.40.1",
        "In-Reply-To": "<20230512192540.401-2-jspewock@iol.unh.edu>",
        "References": "<20230512192540.401-2-jspewock@iol.unh.edu>",
        "MIME-Version": "1.0",
        "Content-Transfer-Encoding": "8bit",
        "X-BeenThere": "dev@dpdk.org",
        "X-Mailman-Version": "2.1.29",
        "Precedence": "list",
        "List-Id": "DPDK patches and discussions <dev.dpdk.org>",
        "List-Unsubscribe": "<https://mails.dpdk.org/options/dev>,\n <mailto:dev-request@dpdk.org?subject=unsubscribe>",
        "List-Archive": "<http://mails.dpdk.org/archives/dev/>",
        "List-Post": "<mailto:dev@dpdk.org>",
        "List-Help": "<mailto:dev-request@dpdk.org?subject=help>",
        "List-Subscribe": "<https://mails.dpdk.org/listinfo/dev>,\n <mailto:dev-request@dpdk.org?subject=subscribe>",
        "Errors-To": "dev-bounces@dpdk.org"
    },
    "content": "From: Jeremy Spewock <jspewock@iol.unh.edu>\n\nAdds a new test suite for running smoke tests that verify general\nconfiguration aspects of the system under test. If any of these tests\nfail, the DTS execution terminates as part of a \"fail-fast\" model.\n\nSigned-off-by: Jeremy Spewock <jspewock@iol.unh.edu>\n---\n dts/conf.yaml                                 |  9 ++\n dts/framework/config/__init__.py              | 21 +++++\n dts/framework/config/conf_yaml_schema.json    | 32 ++++++-\n dts/framework/dts.py                          | 19 +++-\n dts/framework/exception.py                    | 11 +++\n dts/framework/remote_session/os_session.py    |  6 +-\n .../remote_session/remote/__init__.py         | 28 ++++++\n dts/framework/test_result.py                  | 13 ++-\n dts/framework/test_suite.py                   | 24 ++++-\n dts/framework/testbed_model/__init__.py       |  5 +\n .../interactive_apps/__init__.py              |  6 ++\n .../interactive_apps/interactive_command.py   | 57 +++++++++++\n .../interactive_apps/testpmd_driver.py        | 24 +++++\n dts/framework/testbed_model/node.py           |  2 +\n dts/framework/testbed_model/sut_node.py       |  6 ++\n dts/tests/TestSuite_smoke_tests.py            | 94 +++++++++++++++++++\n 16 files changed, 348 insertions(+), 9 deletions(-)\n create mode 100644 dts/framework/testbed_model/interactive_apps/__init__.py\n create mode 100644 dts/framework/testbed_model/interactive_apps/interactive_command.py\n create mode 100644 dts/framework/testbed_model/interactive_apps/testpmd_driver.py\n create mode 100644 dts/tests/TestSuite_smoke_tests.py",
    "diff": "diff --git a/dts/conf.yaml b/dts/conf.yaml\nindex a9bd8a3e..042ef954 100644\n--- a/dts/conf.yaml\n+++ b/dts/conf.yaml\n@@ -10,13 +10,22 @@ executions:\n         compiler_wrapper: ccache\n     perf: false\n     func: true\n+    nics: #physical devices to be used for testing\n+      - addresses:\n+          - \"0000:11:00.0\"\n+          - \"0000:11:00.1\"\n+        driver: \"i40e\"\n+    vdevs: #names of virtual devices to be used for testing\n+      - \"crypto_openssl\"\n     test_suites:\n+      - smoke_tests\n       - hello_world\n     system_under_test: \"SUT 1\"\n nodes:\n   - name: \"SUT 1\"\n     hostname: sut1.change.me.localhost\n     user: root\n+    password: \"\"\n     arch: x86_64\n     os: linux\n     lcores: \"\"\ndiff --git a/dts/framework/config/__init__.py b/dts/framework/config/__init__.py\nindex ebb0823f..f3b8b6e3 100644\n--- a/dts/framework/config/__init__.py\n+++ b/dts/framework/config/__init__.py\n@@ -106,6 +106,21 @@ def from_dict(d: dict) -> \"NodeConfiguration\":\n             hugepages=hugepage_config,\n         )\n \n+@dataclass(slots=True, frozen=True)\n+class NICConfiguration:\n+    addresses: list[str]\n+    driver: str\n+\n+    @staticmethod\n+    def from_dict(d:dict) -> \"NICConfiguration\":\n+        return NICConfiguration(\n+            addresses=[addr for addr in d.get(\"addresses\", [])],\n+            driver=d.get(\"driver\")\n+        )\n+    @staticmethod\n+    def from_list(l:list[dict]) -> list[\"NICConfiguration\"]:\n+        return [] + [NICConfiguration.from_dict(x) for x in l]\n+\n \n @dataclass(slots=True, frozen=True)\n class BuildTargetConfiguration:\n@@ -157,6 +172,8 @@ class ExecutionConfiguration:\n     func: bool\n     test_suites: list[TestSuiteConfig]\n     system_under_test: NodeConfiguration\n+    nics: list[NICConfiguration]\n+    vdevs: list[str]\n \n     @staticmethod\n     def from_dict(d: dict, node_map: dict) -> \"ExecutionConfiguration\":\n@@ -166,7 +183,9 @@ def from_dict(d: dict, node_map: dict) -> \"ExecutionConfiguration\":\n         test_suites: list[TestSuiteConfig] = list(\n             map(TestSuiteConfig.from_dict, d[\"test_suites\"])\n         )\n+        nic_conf: NICConfiguration = NICConfiguration.from_list(d['nics'])\n         sut_name = d[\"system_under_test\"]\n+        list_of_vdevs = d[\"vdevs\"]\n         assert sut_name in node_map, f\"Unknown SUT {sut_name} in execution {d}\"\n \n         return ExecutionConfiguration(\n@@ -174,7 +193,9 @@ def from_dict(d: dict, node_map: dict) -> \"ExecutionConfiguration\":\n             perf=d[\"perf\"],\n             func=d[\"func\"],\n             test_suites=test_suites,\n+            nics=nic_conf,\n             system_under_test=node_map[sut_name],\n+            vdevs=list_of_vdevs\n         )\n \n \ndiff --git a/dts/framework/config/conf_yaml_schema.json b/dts/framework/config/conf_yaml_schema.json\nindex ca2d4a1e..603859de 100644\n--- a/dts/framework/config/conf_yaml_schema.json\n+++ b/dts/framework/config/conf_yaml_schema.json\n@@ -40,6 +40,18 @@\n         \"mscv\"\n       ]\n     },\n+    \"single_nic\" : {\n+      \"type\":\"object\",\n+      \"description\": \"an object that holds nic information\",\n+      \"properties\": {\n+        \"addresses\": {\n+          \"type\":\"array\",\n+          \"items\": {\n+            \"type\":\"string\"\n+          }\n+        }\n+      }\n+    },\n     \"build_target\": {\n       \"type\": \"object\",\n       \"description\": \"Targets supported by DTS\",\n@@ -97,7 +109,8 @@\n     \"test_suite\": {\n       \"type\": \"string\",\n       \"enum\": [\n-        \"hello_world\"\n+        \"hello_world\",\n+        \"smoke_tests\"\n       ]\n     },\n     \"test_target\": {\n@@ -211,6 +224,23 @@\n               ]\n             }\n           },\n+          \"nics\": {\n+            \"type\": \"array\",\n+            \"items\": {\n+              \"oneOf\": [\n+                {\n+                  \"$ref\": \"#/definitions/single_nic\"\n+                }\n+              ]\n+            }\n+          },\n+          \"vdevs\": {\n+            \"description\": \"Names of vdevs to be used in execution\",\n+            \"type\": \"array\",\n+            \"items\": {\n+              \"type\":\"string\"\n+            }\n+          },\n           \"system_under_test\": {\n             \"$ref\": \"#/definitions/node_name\"\n           }\ndiff --git a/dts/framework/dts.py b/dts/framework/dts.py\nindex 05022845..0d03e158 100644\n--- a/dts/framework/dts.py\n+++ b/dts/framework/dts.py\n@@ -5,6 +5,8 @@\n \n import sys\n \n+from .exception import BlockingTestSuiteError\n+\n from .config import CONFIGURATION, BuildTargetConfiguration, ExecutionConfiguration\n from .logger import DTSLOG, getLogger\n from .test_result import BuildTargetResult, DTSResult, ExecutionResult, Result\n@@ -49,6 +51,7 @@ def run_all() -> None:\n                     nodes[sut_node.name] = sut_node\n \n             if sut_node:\n+                #SMOKE TEST EXECUTION GOES HERE!\n                 _run_execution(sut_node, execution, result)\n \n     except Exception as e:\n@@ -118,7 +121,7 @@ def _run_build_target(\n \n     try:\n         sut_node.set_up_build_target(build_target)\n-        result.dpdk_version = sut_node.dpdk_version\n+        # result.dpdk_version = sut_node.dpdk_version\n         build_target_result.update_setup(Result.PASS)\n     except Exception as e:\n         dts_logger.exception(\"Build target setup failed.\")\n@@ -146,6 +149,7 @@ def _run_suites(\n     with possibly only a subset of test cases.\n     If no subset is specified, run all test cases.\n     \"\"\"\n+    end_execution = False\n     for test_suite_config in execution.test_suites:\n         try:\n             full_suite_path = f\"tests.TestSuite_{test_suite_config.test_suite}\"\n@@ -160,13 +164,24 @@ def _run_suites(\n \n         else:\n             for test_suite_class in test_suite_classes:\n+                #HERE NEEDS CHANGING\n                 test_suite = test_suite_class(\n                     sut_node,\n                     test_suite_config.test_cases,\n                     execution.func,\n                     build_target_result,\n+                    sut_node._build_target_config,\n+                    result\n                 )\n-                test_suite.run()\n+                try:\n+                    test_suite.run()\n+                except BlockingTestSuiteError as e:\n+                    dts_logger.exception(\"An error occurred within a blocking TestSuite, execution will now end.\")\n+                    result.add_error(e)\n+                    end_execution = True\n+        #if a blocking test failed and we need to bail out of suite executions\n+        if end_execution:\n+            break\n \n \n def _exit_dts() -> None:\ndiff --git a/dts/framework/exception.py b/dts/framework/exception.py\nindex ca353d98..4e3f63d1 100644\n--- a/dts/framework/exception.py\n+++ b/dts/framework/exception.py\n@@ -25,6 +25,7 @@ class ErrorSeverity(IntEnum):\n     SSH_ERR = 4\n     DPDK_BUILD_ERR = 10\n     TESTCASE_VERIFY_ERR = 20\n+    BLOCKING_TESTSUITE_ERR = 25\n \n \n class DTSError(Exception):\n@@ -144,3 +145,13 @@ def __init__(self, value: str):\n \n     def __str__(self) -> str:\n         return repr(self.value)\n+\n+class BlockingTestSuiteError(DTSError):\n+    suite_name: str\n+    severity: ClassVar[ErrorSeverity]  = ErrorSeverity.BLOCKING_TESTSUITE_ERR\n+\n+    def __init__(self, suite_name:str) -> None:\n+        self.suite_name = suite_name\n+\n+    def __str__(self) -> str:\n+        return f\"Blocking suite {self.suite_name} failed.\"\ndiff --git a/dts/framework/remote_session/os_session.py b/dts/framework/remote_session/os_session.py\nindex 4c48ae25..22776bc1 100644\n--- a/dts/framework/remote_session/os_session.py\n+++ b/dts/framework/remote_session/os_session.py\n@@ -12,7 +12,9 @@\n from framework.testbed_model import LogicalCore\n from framework.utils import EnvVarsDict, MesonArgs\n \n-from .remote import CommandResult, RemoteSession, create_remote_session\n+from .remote import CommandResult, RemoteSession, create_remote_session, create_interactive_session\n+\n+from paramiko import SSHClient\n \n \n class OSSession(ABC):\n@@ -26,6 +28,7 @@ class OSSession(ABC):\n     name: str\n     _logger: DTSLOG\n     remote_session: RemoteSession\n+    _interactive_session: SSHClient\n \n     def __init__(\n         self,\n@@ -37,6 +40,7 @@ def __init__(\n         self.name = name\n         self._logger = logger\n         self.remote_session = create_remote_session(node_config, name, logger)\n+        self._interactive_session = create_interactive_session(node_config, name, logger)\n \n     def close(self, force: bool = False) -> None:\n         \"\"\"\ndiff --git a/dts/framework/remote_session/remote/__init__.py b/dts/framework/remote_session/remote/__init__.py\nindex 8a151221..abca8edc 100644\n--- a/dts/framework/remote_session/remote/__init__.py\n+++ b/dts/framework/remote_session/remote/__init__.py\n@@ -9,8 +9,36 @@\n from .remote_session import CommandResult, RemoteSession\n from .ssh_session import SSHSession\n \n+from paramiko import SSHClient, AutoAddPolicy\n+from framework.utils import GREEN\n \n def create_remote_session(\n     node_config: NodeConfiguration, name: str, logger: DTSLOG\n ) -> RemoteSession:\n     return SSHSession(node_config, name, logger)\n+\n+def create_interactive_session(\n+    node_config: NodeConfiguration, name: str, logger: DTSLOG\n+) -> SSHClient:\n+    \"\"\"\n+    Creates a paramiko SSH session that is designed to be used for interactive shells\n+\n+    This session is meant to be used on an \"as needed\" basis and may never be utilized\n+    \"\"\"\n+    client: SSHClient = SSHClient()\n+    client.set_missing_host_key_policy(AutoAddPolicy)\n+    ip: str = node_config.hostname\n+    logger.info(GREEN(f\"Connecting to host {ip}\"))\n+    #Preset to 22 because paramiko doesn't accept None\n+    port: int = 22\n+    if \":\" in node_config.hostname:\n+            ip, port = node_config.hostname.split(\":\")\n+            port = int(port)\n+    client.connect(\n+        ip,\n+        username=node_config.user,\n+        port=port,\n+        password=node_config.password or \"\",\n+        timeout=20 if port else 10\n+    )\n+    return client\ndiff --git a/dts/framework/test_result.py b/dts/framework/test_result.py\nindex 74391982..77202ae2 100644\n--- a/dts/framework/test_result.py\n+++ b/dts/framework/test_result.py\n@@ -8,6 +8,7 @@\n import os.path\n from collections.abc import MutableSequence\n from enum import Enum, auto\n+from typing import Dict\n \n from .config import (\n     OS,\n@@ -67,12 +68,13 @@ class Statistics(dict):\n     Using a dict provides a convenient way to format the data.\n     \"\"\"\n \n-    def __init__(self, dpdk_version):\n+    def __init__(self, output_info: Dict[str, str] | None):\n         super(Statistics, self).__init__()\n         for result in Result:\n             self[result.name] = 0\n         self[\"PASS RATE\"] = 0.0\n-        self[\"DPDK VERSION\"] = dpdk_version\n+        if output_info:\n+            for info_key, info_val in output_info.items(): self[info_key] = info_val\n \n     def __iadd__(self, other: Result) -> \"Statistics\":\n         \"\"\"\n@@ -258,6 +260,7 @@ class DTSResult(BaseResult):\n     \"\"\"\n \n     dpdk_version: str | None\n+    output: dict | None\n     _logger: DTSLOG\n     _errors: list[Exception]\n     _return_code: ErrorSeverity\n@@ -267,6 +270,7 @@ class DTSResult(BaseResult):\n     def __init__(self, logger: DTSLOG):\n         super(DTSResult, self).__init__()\n         self.dpdk_version = None\n+        self.output = None\n         self._logger = logger\n         self._errors = []\n         self._return_code = ErrorSeverity.NO_ERR\n@@ -296,7 +300,10 @@ def process(self) -> None:\n             for error in self._errors:\n                 self._logger.debug(repr(error))\n \n-        self._stats_result = Statistics(self.dpdk_version)\n+        self._stats_result = Statistics(self.output)\n+        #add information gathered from the smoke tests to the statistics\n+        # for info_key, info_val in smoke_test_info.items(): self._stats_result[info_key] = info_val\n+        # print(self._stats_result)\n         self.add_stats(self._stats_result)\n         with open(self._stats_filename, \"w+\") as stats_file:\n             stats_file.write(str(self._stats_result))\ndiff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py\nindex 0705f38f..1518fb8a 100644\n--- a/dts/framework/test_suite.py\n+++ b/dts/framework/test_suite.py\n@@ -10,11 +10,14 @@\n import inspect\n import re\n from types import MethodType\n+from typing import Dict\n \n-from .exception import ConfigurationError, SSHTimeoutError, TestCaseVerifyError\n+from .config import BuildTargetConfiguration\n+\n+from .exception import BlockingTestSuiteError, ConfigurationError, SSHTimeoutError, TestCaseVerifyError\n from .logger import DTSLOG, getLogger\n from .settings import SETTINGS\n-from .test_result import BuildTargetResult, Result, TestCaseResult, TestSuiteResult\n+from .test_result import BuildTargetResult, DTSResult, Result, TestCaseResult, TestSuiteResult\n from .testbed_model import SutNode\n \n \n@@ -37,10 +40,12 @@ class TestSuite(object):\n     \"\"\"\n \n     sut_node: SutNode\n+    is_blocking = False\n     _logger: DTSLOG\n     _test_cases_to_run: list[str]\n     _func: bool\n     _result: TestSuiteResult\n+    _dts_result: DTSResult\n \n     def __init__(\n         self,\n@@ -48,6 +53,8 @@ def __init__(\n         test_cases: list[str],\n         func: bool,\n         build_target_result: BuildTargetResult,\n+        build_target_conf: BuildTargetConfiguration,\n+        dts_result: DTSResult\n     ):\n         self.sut_node = sut_node\n         self._logger = getLogger(self.__class__.__name__)\n@@ -55,6 +62,8 @@ def __init__(\n         self._test_cases_to_run.extend(SETTINGS.test_cases)\n         self._func = func\n         self._result = build_target_result.add_test_suite(self.__class__.__name__)\n+        self.build_target_info = build_target_conf\n+        self._dts_result = dts_result\n \n     def set_up_suite(self) -> None:\n         \"\"\"\n@@ -118,6 +127,9 @@ def run(self) -> None:\n                     f\"the next test suite may be affected.\"\n                 )\n                 self._result.update_setup(Result.ERROR, e)\n+            if len(self._result.get_errors()) > 0 and self.is_blocking:\n+                raise BlockingTestSuiteError(test_suite_name)\n+            \n \n     def _execute_test_suite(self) -> None:\n         \"\"\"\n@@ -137,6 +149,7 @@ def _execute_test_suite(self) -> None:\n                         f\"Attempt number {attempt_nr} out of {all_attempts}.\"\n                     )\n                     self._run_test_case(test_case_method, test_case_result)\n+                \n \n     def _get_functional_test_cases(self) -> list[MethodType]:\n         \"\"\"\n@@ -232,6 +245,11 @@ def _execute_test_case(\n             test_case_result.update(Result.SKIP)\n             raise KeyboardInterrupt(\"Stop DTS\")\n \n+    def write_to_statistics_file(self, output: Dict[str, str]):\n+        if self._dts_result.output != None:\n+            self._dts_result.output.update(output)\n+        else:\n+            self._dts_result.output = output\n \n def get_test_suites(testsuite_module_path: str) -> list[type[TestSuite]]:\n     def is_test_suite(object) -> bool:\n@@ -252,3 +270,5 @@ def is_test_suite(object) -> bool:\n         test_suite_class\n         for _, test_suite_class in inspect.getmembers(testcase_module, is_test_suite)\n     ]\n+\n+\ndiff --git a/dts/framework/testbed_model/__init__.py b/dts/framework/testbed_model/__init__.py\nindex f54a9470..63f17cc3 100644\n--- a/dts/framework/testbed_model/__init__.py\n+++ b/dts/framework/testbed_model/__init__.py\n@@ -20,3 +20,8 @@\n )\n from .node import Node\n from .sut_node import SutNode\n+\n+from .interactive_apps import (\n+    InteractiveScriptHandler,\n+    TestpmdDriver\n+)\ndiff --git a/dts/framework/testbed_model/interactive_apps/__init__.py b/dts/framework/testbed_model/interactive_apps/__init__.py\nnew file mode 100644\nindex 00000000..0382d7e0\n--- /dev/null\n+++ b/dts/framework/testbed_model/interactive_apps/__init__.py\n@@ -0,0 +1,6 @@\n+from .interactive_command import (\n+    InteractiveScriptHandler\n+)\n+from .testpmd_driver import (\n+    TestpmdDriver\n+)\n\\ No newline at end of file\ndiff --git a/dts/framework/testbed_model/interactive_apps/interactive_command.py b/dts/framework/testbed_model/interactive_apps/interactive_command.py\nnew file mode 100644\nindex 00000000..7467911b\n--- /dev/null\n+++ b/dts/framework/testbed_model/interactive_apps/interactive_command.py\n@@ -0,0 +1,57 @@\n+# import paramiko\n+from paramiko import SSHClient, Channel, channel\n+from framework.settings import SETTINGS\n+\n+class InteractiveScriptHandler:\n+\n+    _ssh_client: SSHClient\n+    _stdin: channel.ChannelStdinFile\n+    _ssh_channel: Channel\n+\n+    def __init__(self, ssh_client: SSHClient, timeout:float = SETTINGS.timeout) -> None:\n+        self._ssh_client = ssh_client\n+        self._ssh_channel = self._ssh_client.invoke_shell()\n+        self._stdin = self._ssh_channel.makefile_stdin(\"wb\")\n+        self._ssh_channel.settimeout(timeout)\n+\n+    def send_command(self, command:str) -> None:\n+        \"\"\"\n+        Send command to channel without recording output.\n+\n+        This method will not verify any input or output, it will\n+        simply assume the command succeeded\n+        \"\"\"\n+        self._stdin.write(command + '\\n')\n+        self._stdin.flush()\n+\n+    def send_command_get_output(self, command:str, expect:str) -> str:\n+        \"\"\"\n+        Send a command and get all output before the expected ending string.\n+\n+        **NOTE**\n+        Lines that expect input are not included in the stdout buffer so they cannot be\n+        used for expect. For example, if you were prompted to log into something\n+        with a username and password, you cannot expect \"username:\" because it wont\n+        yet be in the stdout buffer. A work around for this could be consuming an\n+        extra newline character to force the current prompt into the stdout buffer.\n+\n+        *Return*\n+            All output before expected string\n+        \"\"\"\n+        stdout = self._ssh_channel.makefile(\"r\")\n+        self._stdin.write(command + '\\n')\n+        self._stdin.flush()\n+        out:str = \"\"\n+        for line in stdout:\n+            out += str(line)\n+            if expect in str(line):\n+                break\n+        stdout.close() #close the buffer to flush the output\n+        return out\n+\n+    def close(self):\n+        self._stdin.close()\n+        self._ssh_channel.close()\n+\n+    def __del__(self):\n+        self.close()\ndiff --git a/dts/framework/testbed_model/interactive_apps/testpmd_driver.py b/dts/framework/testbed_model/interactive_apps/testpmd_driver.py\nnew file mode 100644\nindex 00000000..1993eae6\n--- /dev/null\n+++ b/dts/framework/testbed_model/interactive_apps/testpmd_driver.py\n@@ -0,0 +1,24 @@\n+from framework.testbed_model.interactive_apps import InteractiveScriptHandler\n+\n+from pathlib import PurePath\n+\n+class TestpmdDriver:\n+    prompt:str = \"testpmd>\"\n+    interactive_handler: InteractiveScriptHandler\n+\n+    def __init__(self, handler: InteractiveScriptHandler, dpdk_build_dir:PurePath, eal_flags:str = \"\", cmd_line_options:str = \"\") -> None:\n+        \"\"\"\n+        Sets the handler to drive the SSH session and starts testpmd\n+        \"\"\"\n+        self.interactive_handler = handler\n+        # self.interactive_handler.send_command(\"sudo su\")\n+        # self.interactive_handler.send_command(\"cd /root/testpmd-testing/dpdk/build\")\n+        self.interactive_handler.send_command_get_output(f\"{dpdk_build_dir}/app/dpdk-testpmd {eal_flags} -- -i {cmd_line_options}\\n\", self.prompt)\n+\n+    def send_command(self, command:str, expect:str = prompt) -> str:\n+        \"\"\"\n+        Specific way of handling the command for testpmd\n+\n+        An extra newline character is consumed in order to force the current line into the stdout buffer\n+        \"\"\"\n+        return self.interactive_handler.send_command_get_output(command + \"\\n\", expect)\n\\ No newline at end of file\ndiff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py\nindex d48fafe6..c5147e0e 100644\n--- a/dts/framework/testbed_model/node.py\n+++ b/dts/framework/testbed_model/node.py\n@@ -40,6 +40,7 @@ class Node(object):\n     lcores: list[LogicalCore]\n     _logger: DTSLOG\n     _other_sessions: list[OSSession]\n+    _execution_config: ExecutionConfiguration\n \n     def __init__(self, node_config: NodeConfiguration):\n         self.config = node_config\n@@ -64,6 +65,7 @@ def set_up_execution(self, execution_config: ExecutionConfiguration) -> None:\n         \"\"\"\n         self._setup_hugepages()\n         self._set_up_execution(execution_config)\n+        self._execution_config = execution_config\n \n     def _set_up_execution(self, execution_config: ExecutionConfiguration) -> None:\n         \"\"\"\ndiff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py\nindex 2b2b50d9..8c39a66d 100644\n--- a/dts/framework/testbed_model/sut_node.py\n+++ b/dts/framework/testbed_model/sut_node.py\n@@ -14,6 +14,7 @@\n \n from .hw import LogicalCoreCount, LogicalCoreList, VirtualDevice\n from .node import Node\n+from .interactive_apps import InteractiveScriptHandler\n \n \n class SutNode(Node):\n@@ -261,6 +262,11 @@ def run_dpdk_app(\n         return self.main_session.send_command(\n             f\"{app_path} {eal_args}\", timeout, verify=True\n         )\n+    def create_interactive_session_handler(self) -> InteractiveScriptHandler:\n+        \"\"\"\n+        Create a handler for interactive sessions\n+        \"\"\"\n+        return InteractiveScriptHandler(self.main_session._interactive_session)\n \n \n class EalParameters(object):\ndiff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py\nnew file mode 100644\nindex 00000000..bacf289d\n--- /dev/null\n+++ b/dts/tests/TestSuite_smoke_tests.py\n@@ -0,0 +1,94 @@\n+from framework.test_suite import TestSuite\n+from framework.testbed_model.sut_node import SutNode\n+\n+from framework.testbed_model.interactive_apps import TestpmdDriver\n+\n+def get_compiler_version(compiler_name: str, sut_node: SutNode) -> str:\n+    match compiler_name:\n+            case \"gcc\":\n+                return sut_node.main_session.send_command(f\"{compiler_name} --version\", 60).stdout.split(\"\\n\")[0]\n+            case \"clang\":\n+                return sut_node.main_session.send_command(f\"{compiler_name} --version\", 60).stdout.split(\"\\n\")[0]\n+            case \"msvc\":\n+                return sut_node.main_session.send_command(f\"cl\", 60).stdout\n+            case \"icc\":\n+                return sut_node.main_session.send_command(f\"{compiler_name} -V\", 60).stdout\n+\n+class SmokeTests(TestSuite):\n+    is_blocking = True\n+\n+    def set_up_suite(self) -> None:\n+        \"\"\"\n+        Setup:\n+            build all DPDK\n+        \"\"\"\n+        self.dpdk_build_dir_path = self.sut_node.remote_dpdk_build_dir\n+    \n+\n+    def test_unit_tests(self) -> None:\n+        \"\"\"\n+        Test:\n+            run the fast-test unit-test suite through meson\n+        \"\"\"\n+        self.sut_node.main_session.send_command(f\"meson test -C {self.dpdk_build_dir_path} --suite fast-tests\", 300)\n+\n+    def test_driver_tests(self) -> None:\n+        \"\"\"\n+        Test:\n+            run the driver-test unit-test suite through meson\n+        \"\"\"\n+        list_of_vdevs = \"\"\n+        for dev in self.sut_node._execution_config.vdevs:\n+            list_of_vdevs += f\"{dev},\"\n+        print(list_of_vdevs)\n+        if len(list_of_vdevs) > 0:\n+            self.sut_node.main_session.send_command(f\"meson test -C {self.dpdk_build_dir_path} --suite driver-tests --test-args \\\"--vdev {list_of_vdevs}\\\"\", 300)\n+        else:\n+            self.sut_node.main_session.send_command(f\"meson test -C {self.dpdk_build_dir_path} --suite driver-tests\", 300)\n+\n+    def test_gather_info(self) -> None:\n+        \"\"\"\n+        Test:\n+            gather information about the system and send output to statistics.txt\n+        \"\"\"\n+        out = {}\n+        \n+        out['OS'] = self.sut_node.main_session.send_command(\"awk -F= '$1==\\\"NAME\\\" {print $2}' /etc/os-release\", 60).stdout\n+        out[\"OS VERSION\"] = self.sut_node.main_session.send_command(\"awk -F= '$1==\\\"VERSION\\\" {print $2}' /etc/os-release\", 60, True).stdout\n+        out[\"COMPILER VERSION\"] = get_compiler_version(self.build_target_info.compiler.name, self.sut_node)\n+        out[\"DPDK VERSION\"] = self.sut_node.dpdk_version\n+        if self.build_target_info.os.name == \"linux\":\n+            out['KERNEL VERSION'] = self.sut_node.main_session.send_command(\"uname -r\", 60).stdout\n+        elif self.build_target_info.os.name == \"windows\":\n+            out['KERNEL VERSION'] = self.sut_node.main_session.send_command(\"uname -a\", 60).stdout\n+        self.write_to_statistics_file(out)\n+\n+\n+\n+    def test_start_testpmd(self) -> None:\n+        \"\"\"\n+        Creates and instance of the testpmd driver to run the testpmd app\n+        \"\"\"\n+        driver: TestpmdDriver = TestpmdDriver(self.sut_node.create_interactive_session_handler(), self.dpdk_build_dir_path)\n+\n+        print(driver.send_command(\"show port summary all\"))\n+\n+    def test_device_bound_to_driver(self) -> None:\n+        \"\"\"\n+        Test:\n+            ensure that all drivers listed in the config are bound to the correct drivers\n+        \"\"\"\n+        for nic in self.sut_node._execution_config.nics:\n+            for address in nic.addresses:\n+                out = self.sut_node.main_session.send_command(f\"{self.dpdk_build_dir_path}/../usertools/dpdk-devbind.py --status | grep {address}\", 60)\n+                self.verify(\n+                    len(out.stdout) != 0,\n+                    f\"Failed to find configured device ({address}) using dpdk-devbind.py\",\n+                )\n+                for string in out.stdout.split(\" \"):\n+                    if 'drv=' in string:\n+                        self.verify(\n+                            string.split(\"=\")[1] == nic.driver.strip(),\n+                            f'Driver for device {address} does not match driver listed in configuration (bound to {string.split(\"=\")[1]})',\n+                        )\n+        \n\\ No newline at end of file\n",
    "prefixes": [
        "RFC",
        "v2",
        "1/2"
    ]
}