From: "Juraj Linkeš" <juraj.linkes@pantheon.tech>
To: jspewock@iol.unh.edu
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 v8 1/1] dts: add smoke tests
Date: Tue, 18 Jul 2023 11:41:25 +0200 [thread overview]
Message-ID: <CAOb5WZbiE6PUwwPd-SLRh41wVPoz2XvHHhz-M7H+DzRWexcPEQ@mail.gmail.com> (raw)
In-Reply-To: <20230717193705.26594-3-jspewock@iol.unh.edu>
You forgot to add me to the patch :-)
A few comments below.
On Mon, Jul 17, 2023 at 9:37 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>
> ---
<snip>
> 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 0000000000..4d9c7638a5
> --- /dev/null
> +++ b/dts/framework/remote_session/remote/interactive_shell.py
> @@ -0,0 +1,98 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2023 University of New Hampshire
> +
> +from pathlib import PurePath
> +from typing import Callable
> +
> +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
> + _startup_command: str
> + _app_args: str
> + _default_prompt: str = ""
> + _privileged: bool
> + _get_privileged_command: Callable[[str], str]
> + # Allows for app specific extra characters to be appended to commands
> + _command_extra_chars: str = ""
> + path: PurePath
> + dpdk_app: bool = False
> +
We should document this class - let's use the google format:
https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings
You can use this as an example:
http://patches.dpdk.org/project/dpdk/patch/20230511091408.236638-5-juraj.linkes@pantheon.tech/
A note on class attributes: we should document the public ones as well
as those which should be considered to be overridden in derived
classes.
> + def __init__(
> + self,
> + interactive_session: SSHClient,
> + logger: DTSLOG,
> + startup_command: str,
> + privileged: bool,
> + _get_privileged_command: Callable[[str], str],
> + app_args: str = "",
> + timeout: float = SETTINGS.timeout,
> + ) -> None:
We can simplify the constructor signature a bit. We can combine
privileged and _get_privileged_command (which shouldn't start with the
underscore - only private class attributes should have that) in that
get_privileged_command could be None (which would be the same as
privileged=False and not None the same as privileged=True).
We don't need startup_command as we're just passing self.path to it,
which we already have access to.
There is an alternative approach to the path manipulation logic - we
don't modify shell_cls.path at all and pass the resulting path (the
result of all the operations we do in Node objects) to the constructor
(in this case we'd retain the startup_command parameter, although it
should be then renamed to path_app or something similar). This doesn't
move the logic entirely out of InteractiveShell though, as we still
need to access the class attribute. For that reason, I don't see much
of a point in pursuing this alternative - we'd basically be just
passing one extra unneeded path argument through all the calls.
> + 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._startup_command = startup_command
> + self._app_args = app_args
> + self._get_privileged_command = _get_privileged_command # type: ignore
We can actually make this work with a static method:
1. Don't store the method in this object. We'd need to pass it to
_start_application, but this should be ok as we're only using the
method when starting the application - once it's running, we'll never
need it again.
2. When we stored the method, we were passing the self parameter when
calling self._get_privileged_command, which is why it couldn't be
static. When calling without self, static works.
Note: the staticmethod decorator must be above the
abstractmethod decorator, otherwise the interpreter throws an error.
3. The type can be either Callable[[str], str] or MethodType (from the
types module). I'd go with callable here as it's more precise.
> + self._privileged = privileged
> + self._start_application()
> +
> + def _start_application(self) -> None:
> + """Starts a new interactive application based on _startup_command.
> +
> + This method is often overridden by subclasses as their process for
> + starting may look different.
> + """
> + start_command = f"{self._startup_command} {self._app_args}"
> + if self._privileged:
> + start_command = self._get_privileged_command(start_command) # type: ignore
> + self.send_command(start_command)
> +
> + def send_command(self, command: str, prompt: str | None = None) -> 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
My IDE spell checker says there should be a comma before so.
> + 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
Workaround without the space.
> + 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()}...")
> + if prompt is None:
> + prompt = self._default_prompt
> + self._stdin.write(f"{command}{self._command_extra_chars}\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()
<snip>
> diff --git a/dts/framework/test_result.py b/dts/framework/test_result.py
> index 743919820c..724e7328eb 100644
> --- a/dts/framework/test_result.py
> +++ b/dts/framework/test_result.py
<snip>
> @@ -213,6 +218,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:
Just noticed the name - rename to add_build_target_info to align with
everything else.
> + 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)
> diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
> index d48fafe65d..39237a95d6 100644
> --- a/dts/framework/testbed_model/node.py
> +++ b/dts/framework/testbed_model/node.py
> @@ -7,7 +7,7 @@
> A node is a generic host that DTS connects to and manages.
> """
>
> -from typing import Any, Callable
> +from typing import Any, Callable, Type
>
> from framework.config import (
> BuildTargetConfiguration,
> @@ -15,7 +15,7 @@
> NodeConfiguration,
> )
> from framework.logger import DTSLOG, getLogger
> -from framework.remote_session import OSSession, create_session
> +from framework.remote_session import InteractiveShellType, OSSession, create_session
> from framework.settings import SETTINGS
>
> from .hw import (
> @@ -23,6 +23,7 @@
> LogicalCoreCount,
> LogicalCoreList,
> LogicalCoreListFilter,
> + VirtualDevice,
> lcore_filter,
> )
>
> @@ -40,6 +41,8 @@ class Node(object):
> lcores: list[LogicalCore]
> _logger: DTSLOG
> _other_sessions: list[OSSession]
> + _execution_config: ExecutionConfiguration
> + _virtual_devices: list[VirtualDevice]
>
> def __init__(self, node_config: NodeConfiguration):
> self.config = node_config
> @@ -54,6 +57,7 @@ def __init__(self, node_config: NodeConfiguration):
> ).filter()
>
> self._other_sessions = []
> + self._virtual_devices = []
This is not a private attribute, so let's drop the starting underscore.
>
> self._logger.info(f"Created node: {self.name}")
>
> @@ -64,6 +68,9 @@ def set_up_execution(self, execution_config: ExecutionConfiguration) -> None:
> """
> self._setup_hugepages()
> self._set_up_execution(execution_config)
> + self._execution_config = execution_config
> + for vdev in execution_config.vdevs:
> + self._virtual_devices.append(VirtualDevice(vdev))
>
> def _set_up_execution(self, execution_config: ExecutionConfiguration) -> None:
> """
> @@ -77,6 +84,7 @@ def tear_down_execution(self) -> None:
> this node is part of concludes.
> """
> self._tear_down_execution()
> + self._virtual_devices = []
I'd prefer this to be above self._tear_down_execution(). If there's a
failure in self._tear_down_execution(), the devices won't be removed
so removing them before the call is safer.
>
> def _tear_down_execution(self) -> None:
> """
<snip>
> diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py
> new file mode 100644
> index 0000000000..83712e75c6
> --- /dev/null
> +++ b/dts/tests/TestSuite_smoke_tests.py
> @@ -0,0 +1,114 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2023 University of New Hampshire
> +
> +import re
> +
> +from framework.config import PortConfig
> +from framework.remote_session import TestPmdShell
> +from framework.settings import SETTINGS
> +from framework.test_suite import TestSuite
> +from framework.utils import REGEX_FOR_PCI_ADDRESS
> +
> +
> +class SmokeTests(TestSuite):
> + is_blocking = True
> + # dicts in this list are expected to have two keys:
> + # "pci_address" and "current_driver"
> + nics_in_node: list[PortConfig] = []
> +
> + def set_up_suite(self) -> None:
> + """
> + Setup:
> + Set the build directory path and generate a list of NICs in the SUT node.
> + """
> + self.dpdk_build_dir_path = self.sut_node.remote_dpdk_build_dir
> + self.nics_in_node = self.sut_node.config.ports
> +
> + def test_unit_tests(self) -> None:
> + """
> + Test:
> + Run the fast-test unit-test suite through meson.
> + """
> + self.sut_node.main_session.send_command(
> + f"meson test -C {self.dpdk_build_dir_path} --suite fast-tests -t 60",
> + 480,
> + verify=True,
> + privileged=True,
> + )
> +
> + def test_driver_tests(self) -> None:
> + """
> + Test:
> + Run the driver-test unit-test suite through meson.
> + """
> + vdev_args = ""
> + for dev in self.sut_node._virtual_devices:
> + vdev_args += f"--vdev {dev} "
> + vdev_args = vdev_args[:-1]
> + driver_tests_command = (
> + f"meson test -C {self.dpdk_build_dir_path} --suite driver-tests"
> + )
> + if vdev_args:
> + self._logger.info(
> + "Running driver tests with the following virtual "
> + f"devices: {vdev_args}"
> + )
> + driver_tests_command += f' --test-args "{vdev_args}"'
> +
> + self.sut_node.main_session.send_command(
> + driver_tests_command,
> + 300,
> + verify=True,
> + privileged=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(
> + TestPmdShell, privileged=True
> + )
> + dev_list: list[str] = [str(x) for x in testpmd_driver.get_devices()]
The type hint is not necessary, at least according to my mypy. We can
keep it, but we don't do this anywhere else, so I'd go for
consistency.
> + for nic in self.nics_in_node:
> + self.verify(
> + nic.pci in dev_list,
> + f"Device {nic.pci} was not listed in testpmd's available devices, "
> + "please check your configuration",
> + )
> +
> + def test_device_bound_to_driver(self) -> None:
> + """
> + Test:
> + Ensure that all drivers listed in the config are bound to the correct
> + driver.
> + """
> + path_to_devbind = self.sut_node.main_session.join_remote_path(
> + self.sut_node._remote_dpdk_dir, "usertools", "dpdk-devbind.py"
> + )
> +
> + all_nics_in_dpdk_devbind = self.sut_node.main_session.send_command(
> + f"{path_to_devbind} --status | awk '{REGEX_FOR_PCI_ADDRESS}'",
> + SETTINGS.timeout,
> + ).stdout
> +
> + for nic in self.nics_in_node:
> + # This regular expression finds the line in the above string that starts
> + # with the address for the nic we are on in the loop and then captures the
> + # name of the driver in a group
> + devbind_info_for_nic = re.search(
> + f"{nic.pci}[^\\n]*drv=([\\d\\w]*) [^\\n]*",
> + all_nics_in_dpdk_devbind,
> + )
> + self.verify(
> + devbind_info_for_nic is not None,
> + f"Failed to find configured device ({nic.pci}) using dpdk-devbind.py",
> + )
> + # We know this isn't None, but mypy doesn't
> + assert devbind_info_for_nic is not None
> + self.verify(
> + devbind_info_for_nic.group(1) == nic.os_driver,
> + f"Driver for device {nic.pci} does not match driver listed in "
> + f"configuration (bound to {devbind_info_for_nic.group(1)})",
> + )
> --
> 2.41.0
>
next prev parent reply other threads:[~2023-07-18 9:41 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2023-07-17 19:33 [PATCH v8 0/1] Add DTS " jspewock
2023-07-17 19:33 ` [PATCH v8 1/1] dts: add " jspewock
2023-07-18 9:41 ` Juraj Linkeš [this message]
2023-07-18 15:17 ` Jeremy Spewock
2023-07-18 12:08 ` Juraj Linkeš
2023-07-18 15:18 ` Jeremy Spewock
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=CAOb5WZbiE6PUwwPd-SLRh41wVPoz2XvHHhz-M7H+DzRWexcPEQ@mail.gmail.com \
--to=juraj.linkes@pantheon.tech \
--cc=Honnappa.Nagarahalli@arm.com \
--cc=dev@dpdk.org \
--cc=jspewock@iol.unh.edu \
--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).