<div dir="ltr"><div dir="ltr"><br></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Mon, Jul 17, 2023 at 9:31 PM Jeremy Spewock <<a href="mailto:jspewock@iol.unh.edu">jspewock@iol.unh.edu</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div dir="ltr"><div dir="ltr"><div style="font-family:arial,sans-serif"><br></div></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Mon, Jul 17, 2023 at 10:50 AM Juraj Linkeš <juraj.linkes@pantheon.tech> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">I found additional things while working with the interactive shell code.<br>
<br>
On Thu, Jul 13, 2023 at 6:54 PM <<a href="mailto:jspewock@iol.unh.edu" target="_blank">jspewock@iol.unh.edu</a>> wrote:<br>
><br>
> From: Jeremy Spewock <<a href="mailto:jspewock@iol.unh.edu" target="_blank">jspewock@iol.unh.edu</a>><br>
><br>
> Adds a new test suite for running smoke tests that verify general<br>
> configuration aspects of the system under test. If any of these tests<br>
> fail, the DTS execution terminates as part of a "fail-fast" model.<br>
><br>
> Signed-off-by: Jeremy Spewock <<a href="mailto:jspewock@iol.unh.edu" target="_blank">jspewock@iol.unh.edu</a>><br>
> ---<br>
<br>
<snip><br>
<br>
> diff --git a/dts/framework/config/conf_yaml_schema.json b/dts/framework/config/conf_yaml_schema.json<br>
> index ca2d4a1ef2..61f52b4365 100644<br>
> --- a/dts/framework/config/conf_yaml_schema.json<br>
> +++ b/dts/framework/config/conf_yaml_schema.json<br>
<snip><br>
> @@ -211,8 +332,27 @@<br>
>                ]<br>
>              }<br>
>            },<br>
> +          "skip_smoke_tests": {<br>
> +            "description": "Optional field that allows you to skip smoke testing",<br>
> +            "type": "boolean"<br>
> +          },<br>
>            "system_under_test": {<br>
> -            "$ref": "#/definitions/node_name"<br>
> +            "type":"object",<br>
> +            "properties": {<br>
> +              "node_name": {<br>
> +                "$ref": "#/definitions/node_name"<br>
> +              },<br>
> +              "vdevs": {<br>
> +                "description": "Opentional list of names of vdevs to be used in execution",<br>
<br>
Typo in Opentional<br>
<br>
> +                "type": "array",<br>
> +                "items": {<br>
> +                  "type": "string"<br>
> +                }<br>
> +              }<br>
> +            },<br>
> +            "required": [<br>
> +              "node_name"<br>
> +            ]<br>
>            }<br>
>          },<br>
>          "additionalProperties": false,<br>
> diff --git a/dts/framework/dts.py b/dts/framework/dts.py<br>
> index 0502284580..7b09d8fba8 100644<br>
> --- a/dts/framework/dts.py<br>
> +++ b/dts/framework/dts.py<br>
<snip><br>
> @@ -118,14 +124,15 @@ def _run_build_target(<br>
><br>
>      try:<br>
>          sut_node.set_up_build_target(build_target)<br>
> -        result.dpdk_version = sut_node.dpdk_version<br>
> +        # result.dpdk_version = sut_node.dpdk_version<br>
> +        build_target_result.add_build_target_versions(sut_node.get_build_target_info())<br>
<br>
The additions to execution_result and build_target_result are<br>
inconsistent - one modifies __init__, the other doesn't. The time at<br>
which we have the info available is different, which is the reason for<br>
the inconsistency, but I'd rather make all results classes as<br>
consistent as possible - create from config, add other info after<br>
that.<br>
<br>
>          build_target_result.update_setup(Result.PASS)<br>
>      except Exception as e:<br>
>          dts_logger.exception("Build target setup failed.")<br>
>          build_target_result.update_setup(Result.FAIL, e)<br>
><br>
>      else:<br>
> -        _run_suites(sut_node, execution, build_target_result)<br>
> +        _run_all_suites(sut_node, execution, build_target_result)<br>
><br>
>      finally:<br>
>          try:<br>
<br>
<snip><br>
<br>
> diff --git a/dts/framework/remote_session/remote/__init__.py b/dts/framework/remote_session/remote/__init__.py<br>
> index 8a1512210a..03fd309f2b 100644<br>
> --- a/dts/framework/remote_session/remote/__init__.py<br>
> +++ b/dts/framework/remote_session/remote/__init__.py<br>
> @@ -1,16 +1,26 @@<br>
>  # SPDX-License-Identifier: BSD-3-Clause<br>
>  # Copyright(c) 2023 PANTHEON.tech s.r.o.<br>
> +# Copyright(c) 2023 University of New Hampshire<br>
><br>
>  # pylama:ignore=W0611<br>
><br>
>  from framework.config import NodeConfiguration<br>
>  from framework.logger import DTSLOG<br>
><br>
> +from .interactive_remote_session import InteractiveRemoteSession<br>
> +from .interactive_shell import InteractiveShell<br>
>  from .remote_session import CommandResult, RemoteSession<br>
>  from .ssh_session import SSHSession<br>
> +from .testpmd_shell import TestPmdDevice, TestPmdShell<br>
><br>
><br>
>  def create_remote_session(<br>
>      node_config: NodeConfiguration, name: str, logger: DTSLOG<br>
>  ) -> RemoteSession:<br>
>      return SSHSession(node_config, name, logger)<br>
> +<br>
> +<br>
> +def create_interactive_session(<br>
> +    node_config: NodeConfiguration, name: str, logger: DTSLOG<br>
> +) -> InteractiveRemoteSession:<br>
<br>
The name parameter is not used, remove it please.<br>
<br>
> +    return InteractiveRemoteSession(node_config, logger)<br>
<br>
<snip><br>
<br>
> diff --git a/dts/framework/remote_session/remote/testpmd_shell.py b/dts/framework/remote_session/remote/testpmd_shell.py<br>
> new file mode 100644<br>
> index 0000000000..c0261c00f6<br>
> --- /dev/null<br>
> +++ b/dts/framework/remote_session/remote/testpmd_shell.py<br>
> @@ -0,0 +1,74 @@<br>
> +# SPDX-License-Identifier: BSD-3-Clause<br>
> +# Copyright(c) 2023 University of New Hampshire<br>
> +<br>
> +from pathlib import PurePath<br>
> +<br>
> +from paramiko import SSHClient  # type: ignore<br>
> +<br>
> +from framework.logger import DTSLOG<br>
> +from framework.settings import SETTINGS<br>
> +<br>
> +from .interactive_shell import InteractiveShell<br>
> +<br>
> +<br>
> +class TestPmdDevice(object):<br>
> +    pci_address: str<br>
> +<br>
> +    def __init__(self, pci_address: str):<br>
> +        self.pci_address = pci_address<br>
> +<br>
> +    def __str__(self) -> str:<br>
> +        return self.pci_address<br>
> +<br>
> +<br>
> +class TestPmdShell(InteractiveShell):<br>
> +    expected_prompt: str = "testpmd>"<br>
> +    _eal_flags: str<br>
> +<br>
> +    def __init__(<br>
> +        self,<br>
> +        interactive_session: SSHClient,<br>
> +        logger: DTSLOG,<br>
> +        path_to_testpmd: PurePath,<br>
> +        eal_flags: str,<br>
> +        timeout: float = SETTINGS.timeout,<br>
> +    ) -> None:<br>
> +        """Initializes an interactive testpmd session using specified parameters."""<br>
> +        self._eal_flags = eal_flags<br>
> +<br>
> +        super(TestPmdShell, self).__init__(<br>
> +            interactive_session,<br>
> +            logger=logger,<br>
> +            path_to_app=path_to_testpmd,<br>
> +            timeout=timeout,<br>
> +        )<br>
> +<br>
> +    def _start_application(self) -> None:<br>
> +        """Starts a new interactive testpmd shell using _path_to_app."""<br>
> +        self.send_command(<br>
> +            f"{self._path_to_app} {self._eal_flags} -- -i",<br>
> +        )<br>
> +<br>
> +    def send_command(self, command: str, prompt: str = expected_prompt) -> str:<br>
> +        """Specific way of handling the command for testpmd<br>
> +<br>
> +        An extra newline character is consumed in order to force the current line into<br>
> +        the stdout buffer.<br>
> +        """<br>
> +        return self.send_command_get_output(f"{command}\n", prompt)<br>
> +<br>
<br>
We don't really need two methods with different names which basically<br>
do the same - both send a command and get output.<br>
We could use super() to modify the method in subclasses. Or we could<br>
introduce a new class attribute storing the command suffix, similarly<br>
to expected_prompt.<br>
<br>
A note on the class attributes: we should properly document them in<br>
InteractiveShell so that it's clear what should be considered to be<br>
overridden in derived classes.<br>
<br>
> +    def get_devices(self) -> list[TestPmdDevice]:<br>
> +        """Get a list of device names that are known to testpmd<br>
> +<br>
> +        Uses the device info listed in testpmd and then parses the output to<br>
> +        return only the names of the devices.<br>
> +<br>
> +        Returns:<br>
> +            A list of strings representing device names (e.g. 0000:14:00.1)<br>
> +        """<br>
> +        dev_info: str = self.send_command("show device info all")<br>
> +        dev_list: list[TestPmdDevice] = []<br>
> +        for line in dev_info.split("\n"):<br>
> +            if "device name:" in line.lower():<br>
> +                dev_list.append(TestPmdDevice(line.strip().split(": ")[1].strip()))<br>
<br>
We should pass the full line to TestPmdDevice and process the line in<br>
the class. This splits the logic properly - the full line is needed to<br>
get other information about the device.<br>
<br>
> +        return dev_list<br>
> diff --git a/dts/framework/test_result.py b/dts/framework/test_result.py<br>
> index 743919820c..fe1994673e 100644<br>
> --- a/dts/framework/test_result.py<br>
> +++ b/dts/framework/test_result.py<br>
<snip><br>
> @@ -267,14 +288,17 @@ class DTSResult(BaseResult):<br>
>      def __init__(self, logger: DTSLOG):<br>
>          super(DTSResult, self).__init__()<br>
>          self.dpdk_version = None<br>
> +        self.output = None<br>
>          self._logger = logger<br>
>          self._errors = []<br>
>          self._return_code = ErrorSeverity.NO_ERR<br>
>          self._stats_result = None<br>
>          self._stats_filename = os.path.join(SETTINGS.output_dir, "statistics.txt")<br>
><br>
> -    def add_execution(self, sut_node: NodeConfiguration) -> ExecutionResult:<br>
> -        execution_result = ExecutionResult(sut_node)<br>
> +    def add_execution(<br>
> +        self, sut_node: NodeConfiguration, sut_version_info: NodeInfo<br>
> +    ) -> ExecutionResult:<br>
> +        execution_result = ExecutionResult(sut_node, sut_version_info)<br>
>          self._inner_results.append(execution_result)<br>
>          return execution_result<br>
><br>
> @@ -296,7 +320,8 @@ def process(self) -> None:<br>
>              for error in self._errors:<br>
>                  self._logger.debug(repr(error))<br>
><br>
> -        self._stats_result = Statistics(self.dpdk_version)<br>
> +        self._stats_result = Statistics(self.output)<br>
<br>
By changing this (and the semi-related line 121 in dts.py) we've<br>
changed the behavior a bit. We've talked about removing the part which<br>
modifies self.output (or something similar) and these (and other<br>
related parts of the code) look like the remnants of that change.<br>
Let's remove these as well.<br>
<br>
> +        # add information gathered from the smoke tests to the statistics<br>
>          self.add_stats(self._stats_result)<br>
>          with open(self._stats_filename, "w+") as stats_file:<br>
>              stats_file.write(str(self._stats_result))<br>
<br>
<snip><br>
<br>
> diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py<br>
> new file mode 100644<br>
> index 0000000000..9cf547205f<br>
> --- /dev/null<br>
> +++ b/dts/tests/TestSuite_smoke_tests.py<br>
> @@ -0,0 +1,113 @@<br>
> +# SPDX-License-Identifier: BSD-3-Clause<br>
> +# Copyright(c) 2023 University of New Hampshire<br>
> +<br>
> +import re<br>
> +<br>
> +from framework.config import InteractiveApp, PortConfig<br>
> +from framework.remote_session import TestPmdDevice, TestPmdShell<br>
> +from framework.settings import SETTINGS<br>
> +from framework.test_suite import TestSuite<br>
> +from framework.utils import REGEX_FOR_PCI_ADDRESS<br>
> +<br>
> +<br>
> +class SmokeTests(TestSuite):<br>
> +    is_blocking = True<br>
> +    # dicts in this list are expected to have two keys:<br>
> +    # "pci_address" and "current_driver"<br>
> +    nics_in_node: list[PortConfig] = []<br>
> +<br>
> +    def set_up_suite(self) -> None:<br>
> +        """<br>
> +        Setup:<br>
> +            Set the build directory path and generate a list of NICs in the SUT node.<br>
> +        """<br>
> +        self.dpdk_build_dir_path = self.sut_node.remote_dpdk_build_dir<br>
> +        self.nics_in_node = self.sut_node.config.ports<br>
> +<br>
> +    def test_unit_tests(self) -> None:<br>
> +        """<br>
> +        Test:<br>
> +            Run the fast-test unit-test suite through meson.<br>
> +        """<br>
> +        self.sut_node.main_session.send_command(<br>
> +            f"meson test -C {self.dpdk_build_dir_path} --suite fast-tests",<br>
> +            300,<br>
> +            verify=True,<br>
> +        )<br>
<br>
Just a note: we can just add privileged=True to these calls to make<br>
then run as root.<br>
<br>
> +<br>
> +    def test_driver_tests(self) -> None:<br>
> +        """<br>
> +        Test:<br>
> +            Run the driver-test unit-test suite through meson.<br>
> +        """<br>
> +        list_of_vdevs = ""<br>
<br>
This looks more like a string of vdev args :-). I'd rename it to vdev_args.<br>
<br>
> +        for dev in self.sut_node._execution_config.vdevs:<br>
<br>
The test suite should be working with sut_node objects, not<br>
configuration. We should create the VirtualDevice objects from<br>
_execution_config.vdevs in _set_up_execution and then only work with<br>
those. And maybe reset the list in _tear_down_execution.<br>
<br>
> +            list_of_vdevs += f"--vdev {dev} "<br>
> +        list_of_vdevs = list_of_vdevs[:-1]<br>
> +        if list_of_vdevs:<br>
> +            self._<a href="http://logger.info" rel="noreferrer" target="_blank">logger.info</a>(<br>
> +                "Running driver tests with the following virtual "<br>
> +                f"devices: {list_of_vdevs}"<br>
> +            )<br>
> +            self.sut_node.main_session.send_command(<br>
> +                f"meson test -C {self.dpdk_build_dir_path} --suite driver-tests "<br>
> +                f'--test-args "{list_of_vdevs}"',<br>
> +                300,<br>
> +                verify=True,<br>
> +            )<br>
> +        else:<br>
> +            self.sut_node.main_session.send_command(<br>
> +                f"meson test -C {self.dpdk_build_dir_path} --suite driver-tests",<br>
> +                300,<br>
> +                verify=True,<br>
> +            )<br>
> +<br>
> +    def test_devices_listed_in_testpmd(self) -> None:<br>
> +        """<br>
> +        Test:<br>
> +            Uses testpmd driver to verify that devices have been found by testpmd.<br>
> +        """<br>
> +        testpmd_driver = self.sut_node.create_interactive_shell(InteractiveApp.testpmd)<br>
> +        # We know it should always be a TestPmdShell but mypy doesn't<br>
> +        assert isinstance(testpmd_driver, TestPmdShell)<br>
> +        dev_list: list[TestPmdDevice] = testpmd_driver.get_devices()<br>
> +        for nic in self.nics_in_node:<br>
> +            self.verify(<br>
> +                nic.pci in map(str, dev_list),<br>
<br>
map(str, dev_list) gets called for every nic. I'm more inclined toward<br>
composing the list right when assigning to dev_list. Or you could look<br>
into returning (from get_devices()) a derived List overloading the<br>
__contains__ method :-)<br>
<br>
> +                f"Device {nic.pci} was not listed in testpmd's available devices, "<br>
> +                "please check your configuration",<br>
> +            )<br>
> +<br>
> +    def test_device_bound_to_driver(self) -> None:<br>
> +        """<br>
> +        Test:<br>
> +            Ensure that all drivers listed in the config are bound to the correct driver.<br>
> +        """<br>
> +        path_to_devbind = self.sut_node.main_session.join_remote_path(<br>
> +            self.sut_node._remote_dpdk_dir, "usertools", "dpdk-devbind.py"<br>
> +        )<br>
> +<br>
> +        all_nics_in_dpdk_devbind = self.sut_node.main_session.send_command(<br>
> +            f"{path_to_devbind} --status | awk '{REGEX_FOR_PCI_ADDRESS}'",<br>
> +            SETTINGS.timeout,<br>
> +        ).stdout<br>
> +<br>
> +        for nic in self.nics_in_node:<br>
> +            # This regular expression finds the line in the above string that starts<br>
> +            # with the address for the nic we are on in the loop and then captures the<br>
> +            # name of the driver in a group<br>
> +            devbind_info_for_nic = re.search(<br>
> +                f"{nic.pci}[^\\n]*drv=([\\d\\w]*) [^\\n]*",<br>
> +                all_nics_in_dpdk_devbind,<br>
> +            )<br>
> +            self.verify(<br>
> +                devbind_info_for_nic is not None,<br>
> +                f"Failed to find configured device ({nic.pci}) using dpdk-devbind.py",<br>
> +            )<br>
> +            # We know this isn't None, but mypy doesn't<br>
> +            assert devbind_info_for_nic is not None<br>
> +            self.verify(<br>
> +                devbind_info_for_nic.group(1) == nic.os_driver,<br>
> +                f"Driver for device {nic.pci} does not match driver listed in "<br>
> +                f"configuration (bound to {devbind_info_for_nic.group(1)})",<br>
> +            )<br>
> --<br>
> 2.41.0<br>
><br></blockquote><div><br></div><div style="font-family:arial,sans-serif">Thanks for the comments, I went through and addressed them. As we talked about today, I did also add parts from your patch in regards to the interactive shells and I added the ability for them to use sudo. We had talked about using a static method for this, but this would not work because what type of session you are using is an important distinction. I could not pass the entire OSSession class into the interactive shell, so I instead added just a reference to the method for accessing elevated privileges. I had to add a few # type: ignore lines for this to work due to the issue marked here: <a href="https://github.com/python/mypy/issues/2427" target="_blank">https://github.com/python/mypy/issues/2427</a> . Please see my new version.<br></div></div></div></blockquote><div><br></div><div>I have some more comments on the new version, the main being that we can actually pass a static method (from the proper object thus from the proper class). I think that makes more sense since the method should've been static from the get go. I've explained a bit in the comments and I'm attaching a diff here.</div><div> </div></div></div>