DPDK patches and discussions
 help / color / mirror / Atom feed
* [PATCH 0/6] dts: add testpmd params and statefulness
@ 2024-03-26 19:04 Luca Vizzarro
  2024-03-26 19:04 ` [PATCH 1/6] dts: add parameters data structure Luca Vizzarro
                   ` (11 more replies)
  0 siblings, 12 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-03-26 19:04 UTC (permalink / raw)
  To: dev; +Cc: Juraj Linkeš, Luca Vizzarro

Hello!

Sending in some major work relating to Bugzilla Bug 1371. In a few words
I have created a common data structure to handle command line parameters
for shells. I applied this to the current EalParameters class, and made
it so it's reflected across the interactive shell classes for
consistency. Finally, I have implemented all the testpmd parameters
that are publicly documented in a class, and updated the buffer scatter
test suite to use it. The two statefulness patches are very basic and
hopefully the beginning of tracking some state in each testpmd session.

Here are some things I'd like to discuss about these patches:
- the testpmd params defaults. These are a mix between the declared
  defaults on testpmd doc page and some of which we are aware of. I have
  not used all the defaults declared on the testpmd doc page, because I
  have found some inconsistencies when going through testpmd's source
  code.
- the overall structure of the parameter classes. Each of the parameter
  classes are placed in different files, not following a proper
  structure. I'd be keen to restructure everything, and I am open to
  suggestions.
- most of the docstrings relating to the testpmd parameters class are
  effectively taking from testpmd's doc pages. Would this satisfy our
  needs?
- I've tested the docstrings against Juraj's pending API doc generation
  patches and noticed that union types are not correctly represented
  when these are more than two. Not sure if this is a bug or not, but if
  not, any suggestions on how we could solve this?

Looking forward to hearing your replies!

Best regards,
Luca

Luca Vizzarro (6):
  dts: add parameters data structure
  dts: use Params for interactive shells
  dts: add testpmd shell params
  dts: use testpmd params for scatter test suite
  dts: add statefulness to InteractiveShell
  dts: add statefulness to TestPmdShell

 dts/framework/params.py                       | 232 ++++++
 .../remote_session/interactive_shell.py       |  26 +-
 dts/framework/remote_session/testpmd_shell.py | 680 +++++++++++++++++-
 dts/framework/testbed_model/__init__.py       |   2 +-
 dts/framework/testbed_model/node.py           |   4 +-
 dts/framework/testbed_model/os_session.py     |   4 +-
 dts/framework/testbed_model/sut_node.py       | 106 ++-
 dts/tests/TestSuite_pmd_buffer_scatter.py     |  20 +-
 8 files changed, 972 insertions(+), 102 deletions(-)
 create mode 100644 dts/framework/params.py

-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH 1/6] dts: add parameters data structure
  2024-03-26 19:04 [PATCH 0/6] dts: add testpmd params and statefulness Luca Vizzarro
@ 2024-03-26 19:04 ` Luca Vizzarro
  2024-03-28 16:48   ` Jeremy Spewock
  2024-04-09 12:10   ` Juraj Linkeš
  2024-03-26 19:04 ` [PATCH 2/6] dts: use Params for interactive shells Luca Vizzarro
                   ` (10 subsequent siblings)
  11 siblings, 2 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-03-26 19:04 UTC (permalink / raw)
  To: dev
  Cc: Juraj Linkeš,
	Luca Vizzarro, Jack Bond-Preston, Honnappa Nagarahalli

This commit introduces a new "params" module, which adds a new way
to manage command line parameters. The provided Params dataclass
is able to read the fields of its child class and produce a string
representation to supply to the command line. Any data structure
that is intended to represent command line parameters can inherit
it. The representation can then manipulated by using the dataclass
field metadata in conjunction with the provided functions:

* value_only, used to supply a value without forming an option/flag
* options_end, used to prefix with an options end delimiter (`--`)
* short, used to define a short parameter name, e.g. `-p`
* long, used to define a long parameter name, e.g. `--parameter`
* multiple, used to turn a list into repeating parameters
* field_mixins, used to manipulate the string representation of the
  value

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Jack Bond-Preston <jack.bond-preston@arm.com>
Reviewed-by: Honnappa Nagarahalli <honnappa.nagarahalli@arm.com>
---
 dts/framework/params.py | 232 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 232 insertions(+)
 create mode 100644 dts/framework/params.py

diff --git a/dts/framework/params.py b/dts/framework/params.py
new file mode 100644
index 0000000000..6b48c8353e
--- /dev/null
+++ b/dts/framework/params.py
@@ -0,0 +1,232 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Parameter manipulation module.
+
+This module provides :class:`~Params` which can be used to model any data structure
+that is meant to represent any command parameters.
+"""
+
+from dataclasses import dataclass, field, fields
+from typing import Any, Callable, Literal, Reversible, TypeVar, Iterable
+from enum import Flag
+
+
+T = TypeVar("T")
+#: Type for a Mixin.
+Mixin = Callable[[Any], str]
+#: Type for an option parameter.
+Option = Literal[True, None]
+#: Type for a yes/no option parameter.
+BooleanOption = Literal[True, False, None]
+
+META_VALUE_ONLY = "value_only"
+META_OPTIONS_END = "options_end"
+META_SHORT_NAME = "short_name"
+META_LONG_NAME = "long_name"
+META_MULTIPLE = "multiple"
+META_MIXINS = "mixins"
+
+
+def value_only(metadata: dict[str, Any] = {}) -> dict[str, Any]:
+    """Injects the value of the attribute as-is without flag. Metadata modifier for :func:`dataclasses.field`."""
+    return {**metadata, META_VALUE_ONLY: True}
+
+
+def short(name: str, metadata: dict[str, Any] = {}) -> dict[str, Any]:
+    """Overrides any parameter name with the given short option. Metadata modifier for :func:`dataclasses.field`.
+
+    .. code:: python
+
+        logical_cores: str | None = field(default="1-4", metadata=short("l"))
+
+    will render as ``-l=1-4`` instead of ``--logical-cores=1-4``.
+    """
+    return {**metadata, META_SHORT_NAME: name}
+
+
+def long(name: str, metadata: dict[str, Any] = {}) -> dict[str, Any]:
+    """Overrides the inferred parameter name to the specified one. Metadata modifier for :func:`dataclasses.field`.
+
+    .. code:: python
+
+        x_name: str | None = field(default="y", metadata=long("x"))
+
+    will render as ``--x=y``, but the field is accessed and modified through ``x_name``.
+    """
+    return {**metadata, META_LONG_NAME: name}
+
+
+def options_end(metadata: dict[str, Any] = {}) -> dict[str, Any]:
+    """Precedes the value with an options end delimiter (``--``). Metadata modifier for :func:`dataclasses.field`."""
+    return {**metadata, META_OPTIONS_END: True}
+
+
+def multiple(metadata: dict[str, Any] = {}) -> dict[str, Any]:
+    """Specifies that this parameter is set multiple times. Must be a list. Metadata modifier for :func:`dataclasses.field`.
+
+    .. code:: python
+
+        ports: list[int] | None = field(default_factory=lambda: [0, 1, 2], metadata=multiple(param_name("port")))
+
+    will render as ``--port=0 --port=1 --port=2``. Note that modifiers can be chained like in this example.
+    """
+    return {**metadata, META_MULTIPLE: True}
+
+
+def field_mixins(*mixins: Mixin, metadata: dict[str, Any] = {}) -> dict[str, Any]:
+    """Takes in a variable number of mixins to manipulate the value's rendering. Metadata modifier for :func:`dataclasses.field`.
+
+    The ``metadata`` keyword argument can be used to chain metadata modifiers together.
+
+    Mixins can be chained together, executed from right to left in the arguments list order.
+
+    Example:
+
+    .. code:: python
+
+        hex_bitmask: int | None = field(default=0b1101, metadata=field_mixins(hex, metadata=param_name("mask")))
+
+    will render as ``--mask=0xd``. The :func:`hex` built-in can be used as a mixin turning a valid integer into a hexadecimal representation.
+    """
+    return {**metadata, META_MIXINS: mixins}
+
+
+def _reduce_mixins(mixins: Reversible[Mixin], value: Any) -> str:
+    for mixin in reversed(mixins):
+        value = mixin(value)
+    return value
+
+
+def str_mixins(*mixins: Mixin):
+    """Decorator which modifies the ``__str__`` method, enabling support for mixins.
+
+    Mixins can be chained together, executed from right to left in the arguments list order.
+
+    Example:
+
+    .. code:: python
+
+        @str_mixins(hex_from_flag_value)
+        class BitMask(enum.Flag):
+            A = auto()
+            B = auto()
+
+    will allow ``BitMask`` to render as a hexadecimal value.
+    """
+
+    def _class_decorator(original_class):
+        original_class.__str__ = lambda self: _reduce_mixins(mixins, self)
+        return original_class
+
+    return _class_decorator
+
+
+def comma_separated(values: Iterable[T]) -> str:
+    """Mixin which renders an iterable in a comma-separated string."""
+    return ",".join([str(value).strip() for value in values if value is not None])
+
+
+def bracketed(value: str) -> str:
+    """Mixin which adds round brackets to the input."""
+    return f"({value})"
+
+
+def str_from_flag_value(flag: Flag) -> str:
+    """Mixin which returns the value from a :class:`enum.Flag` as a string."""
+    return str(flag.value)
+
+
+def hex_from_flag_value(flag: Flag) -> str:
+    """Mixin which turns a :class:`enum.Flag` value into hexadecimal."""
+    return hex(flag.value)
+
+
+def _make_option(param_name: str, short: bool = False, no: bool = False) -> str:
+    param_name = param_name.replace("_", "-")
+    return f"{'-' if short else '--'}{'no-' if no else ''}{param_name}"
+
+
+@dataclass
+class Params:
+    """Helper abstract dataclass that renders its fields into command line arguments.
+
+    The parameter name is taken from the field name by default. The following:
+
+    .. code:: python
+
+        name: str | None = "value"
+
+    is rendered as ``--name=value``.
+    Through :func:`dataclasses.field` the resulting parameter can be manipulated by applying
+    appropriate metadata. This class can be used with the following metadata modifiers:
+
+    * :func:`value_only`
+    * :func:`options_end`
+    * :func:`short`
+    * :func:`long`
+    * :func:`multiple`
+    * :func:`field_mixins`
+
+    To use fields as option switches set the value to ``True`` to enable them. If you
+    use a yes/no option switch you can also set ``False`` which would enable an option
+    prefixed with ``--no-``. Examples:
+
+    .. code:: python
+
+        interactive: Option = True  # renders --interactive
+        numa: BooleanOption = False # renders --no-numa
+
+    Setting ``None`` will disable any option. The :attr:`~Option` type alias is provided for
+    regular option switches, whereas :attr:`~BooleanOption` is offered for yes/no ones.
+
+    An instance of a dataclass inheriting ``Params`` can also be assigned to an attribute, this helps with grouping parameters
+    together. The attribute holding the dataclass will be ignored and the latter will just be rendered as expected.
+    """
+
+    def __str__(self) -> str:
+        arguments: list[str] = []
+
+        for field in fields(self):
+            value = getattr(self, field.name)
+
+            if value is None:
+                continue
+
+            options_end = field.metadata.get(META_OPTIONS_END, False)
+            if options_end:
+                arguments.append("--")
+
+            value_only = field.metadata.get(META_VALUE_ONLY, False)
+            if isinstance(value, Params) or value_only or options_end:
+                arguments.append(str(value))
+                continue
+
+            # take "short_name" metadata, or "long_name" metadata, or infer from field name
+            option_name = field.metadata.get(
+                META_SHORT_NAME, field.metadata.get(META_LONG_NAME, field.name)
+            )
+            is_short = META_SHORT_NAME in field.metadata
+
+            if isinstance(value, bool):
+                arguments.append(_make_option(option_name, short=is_short, no=(not value)))
+                continue
+
+            option = _make_option(option_name, short=is_short)
+            separator = " " if is_short else "="
+            str_mixins = field.metadata.get(META_MIXINS, [])
+            multiple = field.metadata.get(META_MULTIPLE, False)
+
+            values = value if multiple else [value]
+            for entry_value in values:
+                entry_value = _reduce_mixins(str_mixins, entry_value)
+                arguments.append(f"{option}{separator}{entry_value}")
+
+        return " ".join(arguments)
+
+
+@dataclass
+class StrParams(Params):
+    """A drop-in replacement for parameters passed as a string."""
+
+    value: str = field(metadata=value_only())
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH 2/6] dts: use Params for interactive shells
  2024-03-26 19:04 [PATCH 0/6] dts: add testpmd params and statefulness Luca Vizzarro
  2024-03-26 19:04 ` [PATCH 1/6] dts: add parameters data structure Luca Vizzarro
@ 2024-03-26 19:04 ` Luca Vizzarro
  2024-03-28 16:48   ` Jeremy Spewock
  2024-05-28 15:43   ` Nicholas Pratte
  2024-03-26 19:04 ` [PATCH 3/6] dts: add testpmd shell params Luca Vizzarro
                   ` (9 subsequent siblings)
  11 siblings, 2 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-03-26 19:04 UTC (permalink / raw)
  To: dev
  Cc: Juraj Linkeš,
	Luca Vizzarro, Jack Bond-Preston, Honnappa Nagarahalli

Make it so that interactive shells accept an implementation of `Params`
for app arguments. Convert EalParameters to use `Params` instead.

String command line parameters can still be supplied by using the
`StrParams` implementation.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Jack Bond-Preston <jack.bond-preston@arm.com>
Reviewed-by: Honnappa Nagarahalli <honnappa.nagarahalli@arm.com>
---
 .../remote_session/interactive_shell.py       |   8 +-
 dts/framework/remote_session/testpmd_shell.py |  12 +-
 dts/framework/testbed_model/__init__.py       |   2 +-
 dts/framework/testbed_model/node.py           |   4 +-
 dts/framework/testbed_model/os_session.py     |   4 +-
 dts/framework/testbed_model/sut_node.py       | 106 ++++++++----------
 dts/tests/TestSuite_pmd_buffer_scatter.py     |   3 +-
 7 files changed, 73 insertions(+), 66 deletions(-)

diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
index 5cfe202e15..a2c7b30d9f 100644
--- a/dts/framework/remote_session/interactive_shell.py
+++ b/dts/framework/remote_session/interactive_shell.py
@@ -1,5 +1,6 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright(c) 2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """Common functionality for interactive shell handling.
 
@@ -21,6 +22,7 @@
 from paramiko import Channel, SSHClient, channel  # type: ignore[import]
 
 from framework.logger import DTSLogger
+from framework.params import Params
 from framework.settings import SETTINGS
 
 
@@ -40,7 +42,7 @@ class InteractiveShell(ABC):
     _ssh_channel: Channel
     _logger: DTSLogger
     _timeout: float
-    _app_args: str
+    _app_args: Params | None
 
     #: Prompt to expect at the end of output when sending a command.
     #: This is often overridden by subclasses.
@@ -63,7 +65,7 @@ def __init__(
         interactive_session: SSHClient,
         logger: DTSLogger,
         get_privileged_command: Callable[[str], str] | None,
-        app_args: str = "",
+        app_args: Params | None = None,
         timeout: float = SETTINGS.timeout,
     ) -> None:
         """Create an SSH channel during initialization.
@@ -100,7 +102,7 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
             get_privileged_command: A function (but could be any callable) that produces
                 the version of the command with elevated privileges.
         """
-        start_command = f"{self.path} {self._app_args}"
+        start_command = f"{self.path} {self._app_args or ''}"
         if get_privileged_command is not None:
             start_command = get_privileged_command(start_command)
         self.send_command(start_command)
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index cb2ab6bd00..db3abb7600 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -21,6 +21,7 @@
 from typing import Callable, ClassVar
 
 from framework.exception import InteractiveCommandExecutionError
+from framework.params import StrParams
 from framework.settings import SETTINGS
 from framework.utils import StrEnum
 
@@ -118,8 +119,15 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
         Also find the number of pci addresses which were allowed on the command line when the app
         was started.
         """
-        self._app_args += " -i --mask-event intr_lsc"
-        self.number_of_ports = self._app_args.count("-a ")
+        from framework.testbed_model.sut_node import EalParameters
+
+        assert isinstance(self._app_args, EalParameters)
+
+        if isinstance(self._app_args.app_params, StrParams):
+            self._app_args.app_params.value += " -i --mask-event intr_lsc"
+
+        self.number_of_ports = len(self._app_args.ports) if self._app_args.ports is not None else 0
+
         super()._start_application(get_privileged_command)
 
     def start(self, verify: bool = True) -> None:
diff --git a/dts/framework/testbed_model/__init__.py b/dts/framework/testbed_model/__init__.py
index 6086512ca2..ef9520df4c 100644
--- a/dts/framework/testbed_model/__init__.py
+++ b/dts/framework/testbed_model/__init__.py
@@ -23,6 +23,6 @@
 from .cpu import LogicalCoreCount, LogicalCoreCountFilter, LogicalCoreList
 from .node import Node
 from .port import Port, PortLink
-from .sut_node import SutNode
+from .sut_node import SutNode, EalParameters
 from .tg_node import TGNode
 from .virtual_device import VirtualDevice
diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
index 74061f6262..ec9512d618 100644
--- a/dts/framework/testbed_model/node.py
+++ b/dts/framework/testbed_model/node.py
@@ -2,6 +2,7 @@
 # Copyright(c) 2010-2014 Intel Corporation
 # Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2022-2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """Common functionality for node management.
 
@@ -24,6 +25,7 @@
 )
 from framework.exception import ConfigurationError
 from framework.logger import DTSLogger, get_dts_logger
+from framework.params import Params
 from framework.settings import SETTINGS
 
 from .cpu import (
@@ -199,7 +201,7 @@ def create_interactive_shell(
         shell_cls: Type[InteractiveShellType],
         timeout: float = SETTINGS.timeout,
         privileged: bool = False,
-        app_args: str = "",
+        app_args: Params | None = None,
     ) -> InteractiveShellType:
         """Factory for interactive session handlers.
 
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
index d5bf7e0401..7234c975c8 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright(c) 2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """OS-aware remote session.
 
@@ -29,6 +30,7 @@
 
 from framework.config import Architecture, NodeConfiguration, NodeInfo
 from framework.logger import DTSLogger
+from framework.params import Params
 from framework.remote_session import (
     CommandResult,
     InteractiveRemoteSession,
@@ -134,7 +136,7 @@ def create_interactive_shell(
         shell_cls: Type[InteractiveShellType],
         timeout: float,
         privileged: bool,
-        app_args: str,
+        app_args: Params | None,
     ) -> InteractiveShellType:
         """Factory for interactive session handlers.
 
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index 97aa26d419..3f8c3807b3 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -2,6 +2,7 @@
 # Copyright(c) 2010-2014 Intel Corporation
 # Copyright(c) 2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """System under test (DPDK + hardware) node.
 
@@ -11,6 +12,7 @@
 """
 
 
+from dataclasses import dataclass, field
 import os
 import tarfile
 import time
@@ -23,6 +25,8 @@
     NodeInfo,
     SutNodeConfiguration,
 )
+from framework import params
+from framework.params import Params, StrParams
 from framework.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
@@ -34,62 +38,51 @@
 from .virtual_device import VirtualDevice
 
 
-class EalParameters(object):
+def _port_to_pci(port: Port) -> str:
+    return port.pci
+
+
+@dataclass(kw_only=True)
+class EalParameters(Params):
     """The environment abstraction layer parameters.
 
     The string representation can be created by converting the instance to a string.
     """
 
-    def __init__(
-        self,
-        lcore_list: LogicalCoreList,
-        memory_channels: int,
-        prefix: str,
-        no_pci: bool,
-        vdevs: list[VirtualDevice],
-        ports: list[Port],
-        other_eal_param: str,
-    ):
-        """Initialize the parameters according to inputs.
-
-        Process the parameters into the format used on the command line.
+    lcore_list: LogicalCoreList = field(metadata=params.short("l"))
+    """The list of logical cores to use."""
 
-        Args:
-            lcore_list: The list of logical cores to use.
-            memory_channels: The number of memory channels to use.
-            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
-            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
-            vdevs: Virtual devices, e.g.::
+    memory_channels: int = field(metadata=params.short("n"))
+    """The number of memory channels to use."""
 
-                vdevs=[
-                    VirtualDevice('net_ring0'),
-                    VirtualDevice('net_ring1')
-                ]
-            ports: The list of ports to allow.
-            other_eal_param: user defined DPDK EAL parameters, e.g.:
-                ``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._ports = " ".join(f"-a {port.pci}" for port in ports)
-        self._other_eal_param = other_eal_param
-
-    def __str__(self) -> str:
-        """Create the EAL string."""
-        return (
-            f"{self._lcore_list} "
-            f"{self._memory_channels} "
-            f"{self._prefix} "
-            f"{self._no_pci} "
-            f"{self._vdevs} "
-            f"{self._ports} "
-            f"{self._other_eal_param}"
-        )
+    prefix: str = field(metadata=params.long("file-prefix"))
+    """Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``."""
+
+    no_pci: params.Option
+    """Switch to disable PCI bus e.g.: ``no_pci=True``."""
+
+    vdevs: list[VirtualDevice] | None = field(
+        default=None, metadata=params.multiple(params.long("vdev"))
+    )
+    """Virtual devices, e.g.::
+
+        vdevs=[
+            VirtualDevice("net_ring0"),
+            VirtualDevice("net_ring1")
+        ]
+    """
+
+    ports: list[Port] | None = field(
+        default=None,
+        metadata=params.field_mixins(_port_to_pci, metadata=params.multiple(params.short("a"))),
+    )
+    """The list of ports to allow."""
+
+    other_eal_param: StrParams | None = None
+    """Any other EAL parameter(s)."""
+
+    app_params: Params | None = field(default=None, metadata=params.options_end())
+    """Parameters to pass to the underlying DPDK app."""
 
 
 class SutNode(Node):
@@ -350,7 +343,7 @@ def create_eal_parameters(
         ascending_cores: bool = True,
         prefix: str = "dpdk",
         append_prefix_timestamp: bool = True,
-        no_pci: bool = False,
+        no_pci: params.Option = None,
         vdevs: list[VirtualDevice] | None = None,
         ports: list[Port] | None = None,
         other_eal_param: str = "",
@@ -393,9 +386,6 @@ def create_eal_parameters(
         if prefix:
             self._dpdk_prefix_list.append(prefix)
 
-        if vdevs is None:
-            vdevs = []
-
         if ports is None:
             ports = self.ports
 
@@ -406,7 +396,7 @@ def create_eal_parameters(
             no_pci=no_pci,
             vdevs=vdevs,
             ports=ports,
-            other_eal_param=other_eal_param,
+            other_eal_param=StrParams(other_eal_param),
         )
 
     def run_dpdk_app(
@@ -442,7 +432,7 @@ def create_interactive_shell(
         shell_cls: Type[InteractiveShellType],
         timeout: float = SETTINGS.timeout,
         privileged: bool = False,
-        app_parameters: str = "",
+        app_parameters: Params | None = None,
         eal_parameters: EalParameters | None = None,
     ) -> InteractiveShellType:
         """Extend the factory for interactive session handlers.
@@ -459,6 +449,7 @@ def create_interactive_shell(
                 reading from the buffer and don't receive any data within the timeout
                 it will throw an error.
             privileged: Whether to run the shell with administrative privileges.
+            app_args: The arguments to be passed to the application.
             eal_parameters: List of EAL parameters to use to launch the app. If this
                 isn't provided or an empty string is passed, it will default to calling
                 :meth:`create_eal_parameters`.
@@ -470,9 +461,10 @@ def create_interactive_shell(
         """
         # We need to append the build directory and add EAL parameters for DPDK apps
         if shell_cls.dpdk_app:
-            if not eal_parameters:
+            if eal_parameters is None:
                 eal_parameters = self.create_eal_parameters()
-            app_parameters = f"{eal_parameters} -- {app_parameters}"
+            eal_parameters.app_params = app_parameters
+            app_parameters = eal_parameters
 
             shell_cls.path = self.main_session.join_remote_path(
                 self.remote_dpdk_build_dir, shell_cls.path
diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index 3701c47408..4cdbdc4272 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -22,6 +22,7 @@
 from scapy.packet import Raw  # type: ignore[import]
 from scapy.utils import hexstr  # type: ignore[import]
 
+from framework.params import StrParams
 from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
 from framework.test_suite import TestSuite
 
@@ -103,7 +104,7 @@ def pmd_scatter(self, mbsize: int) -> None:
         """
         testpmd = self.sut_node.create_interactive_shell(
             TestPmdShell,
-            app_parameters=(
+            app_parameters=StrParams(
                 "--mbcache=200 "
                 f"--mbuf-size={mbsize} "
                 "--max-pkt-len=9000 "
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH 3/6] dts: add testpmd shell params
  2024-03-26 19:04 [PATCH 0/6] dts: add testpmd params and statefulness Luca Vizzarro
  2024-03-26 19:04 ` [PATCH 1/6] dts: add parameters data structure Luca Vizzarro
  2024-03-26 19:04 ` [PATCH 2/6] dts: use Params for interactive shells Luca Vizzarro
@ 2024-03-26 19:04 ` Luca Vizzarro
  2024-03-28 16:48   ` Jeremy Spewock
  2024-04-09 16:37   ` Juraj Linkeš
  2024-03-26 19:04 ` [PATCH 4/6] dts: use testpmd params for scatter test suite Luca Vizzarro
                   ` (8 subsequent siblings)
  11 siblings, 2 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-03-26 19:04 UTC (permalink / raw)
  To: dev
  Cc: Juraj Linkeš,
	Luca Vizzarro, Jack Bond-Preston, Honnappa Nagarahalli

Implement all the testpmd shell parameters into a data structure.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Jack Bond-Preston <jack.bond-preston@arm.com>
Reviewed-by: Honnappa Nagarahalli <honnappa.nagarahalli@arm.com>
---
 dts/framework/remote_session/testpmd_shell.py | 633 +++++++++++++++++-
 1 file changed, 615 insertions(+), 18 deletions(-)

diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index db3abb7600..a823dc53be 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright(c) 2023 University of New Hampshire
 # Copyright(c) 2023 PANTHEON.tech s.r.o.
+# Copyright(c) 2024 Arm Limited
 
 """Testpmd interactive shell.
 
@@ -15,10 +16,25 @@
     testpmd_shell.close()
 """
 
+from dataclasses import dataclass, field
+from enum import auto, Enum, Flag, unique
 import time
-from enum import auto
 from pathlib import PurePath
-from typing import Callable, ClassVar
+from typing import Callable, ClassVar, Literal, NamedTuple
+from framework.params import (
+    BooleanOption,
+    Params,
+    bracketed,
+    comma_separated,
+    Option,
+    field_mixins,
+    hex_from_flag_value,
+    multiple,
+    long,
+    short,
+    str_from_flag_value,
+    str_mixins,
+)
 
 from framework.exception import InteractiveCommandExecutionError
 from framework.params import StrParams
@@ -28,26 +44,79 @@
 from .interactive_shell import InteractiveShell
 
 
-class TestPmdDevice(object):
-    """The data of a device that testpmd can recognize.
+@str_mixins(bracketed, comma_separated)
+class TestPmdPortNUMAConfig(NamedTuple):
+    """DPDK port to NUMA socket association tuple."""
 
-    Attributes:
-        pci_address: The PCI address of the device.
+    port: int
+    socket: int
+
+
+@str_mixins(str_from_flag_value)
+@unique
+class TestPmdFlowDirection(Flag):
+    """Flag indicating the direction of the flow.
+
+    A bi-directional flow can be specified with the pipe:
+
+    >>> TestPmdFlowDirection.RX | TestPmdFlowDirection.TX
+    <TestPmdFlowDirection.TX|RX: 3>
     """
 
-    pci_address: str
+    #:
+    RX = 1 << 0
+    #:
+    TX = 1 << 1
 
-    def __init__(self, pci_address_line: str):
-        """Initialize the device from the testpmd output line string.
 
-        Args:
-            pci_address_line: A line of testpmd output that contains a device.
-        """
-        self.pci_address = pci_address_line.strip().split(": ")[1].strip()
+@str_mixins(bracketed, comma_separated)
+class TestPmdRingNUMAConfig(NamedTuple):
+    """Tuple associating DPDK port, direction of the flow and NUMA socket."""
+
+    port: int
+    direction: TestPmdFlowDirection
+    socket: int
+
+
+@str_mixins(comma_separated)
+class TestPmdEthPeer(NamedTuple):
+    """Tuple associating a MAC address to the specified DPDK port."""
+
+    port_no: int
+    mac_address: str
+
+
+@str_mixins(comma_separated)
+class TestPmdTxIPAddrPair(NamedTuple):
+    """Tuple specifying the source and destination IPs for the packets."""
+
+    source_ip: str
+    dest_ip: str
 
-    def __str__(self) -> str:
-        """The PCI address captures what the device is."""
-        return self.pci_address
+
+@str_mixins(comma_separated)
+class TestPmdTxUDPPortPair(NamedTuple):
+    """Tuple specifying the UDP source and destination ports for the packets.
+
+    If leaving ``dest_port`` unspecified, ``source_port`` will be used for the destination port as well.
+    """
+
+    source_port: int
+    dest_port: int | None = None
+
+
+class TestPmdPortTopology(StrEnum):
+    paired = auto()
+    """In paired mode, the forwarding is between pairs of ports,
+    for example: (0,1), (2,3), (4,5)."""
+    chained = auto()
+    """In chained mode, the forwarding is to the next available port in the port mask,
+    for example: (0,1), (1,2), (2,0).
+
+    The ordering of the ports can be changed using the portlist testpmd runtime function.
+    """
+    loop = auto()
+    """In loop mode, ingress traffic is simply transmitted back on the same interface."""
 
 
 class TestPmdForwardingModes(StrEnum):
@@ -81,6 +150,534 @@ class TestPmdForwardingModes(StrEnum):
     recycle_mbufs = auto()
 
 
+@str_mixins(comma_separated)
+class XYPair(NamedTuple):
+    #:
+    X: int
+    #:
+    Y: int | None = None
+
+
+@str_mixins(hex_from_flag_value)
+@unique
+class TestPmdRXMultiQueueMode(Flag):
+    #:
+    RSS = 1 << 0
+    #:
+    DCB = 1 << 1
+    #:
+    VMDQ = 1 << 2
+
+
+@str_mixins(hex_from_flag_value)
+@unique
+class TestPmdHairpinMode(Flag):
+    TWO_PORTS_LOOP = 1 << 0
+    """Two hairpin ports loop."""
+    TWO_PORTS_PAIRED = 1 << 1
+    """Two hairpin ports paired."""
+    EXPLICIT_TX_FLOW = 1 << 4
+    """Explicit Tx flow rule."""
+    FORCE_RX_QUEUE_MEM_SETTINGS = 1 << 8
+    """Force memory settings of hairpin RX queue."""
+    FORCE_TX_QUEUE_MEM_SETTINGS = 1 << 9
+    """Force memory settings of hairpin TX queue."""
+    RX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 12
+    """Hairpin RX queues will use locked device memory."""
+    RX_QUEUE_USE_RTE_MEMORY = 1 << 13
+    """Hairpin RX queues will use RTE memory."""
+    TX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 16
+    """Hairpin TX queues will use locked device memory."""
+    TX_QUEUE_USE_RTE_MEMORY = 1 << 18
+    """Hairpin TX queues will use RTE memory."""
+
+
+class TestPmdEvent(StrEnum):
+    #:
+    unknown = auto()
+    #:
+    queue_state = auto()
+    #:
+    vf_mbox = auto()
+    #:
+    macsec = auto()
+    #:
+    intr_lsc = auto()
+    #:
+    intr_rmv = auto()
+    #:
+    intr_reset = auto()
+    #:
+    dev_probed = auto()
+    #:
+    dev_released = auto()
+    #:
+    flow_aged = auto()
+    #:
+    err_recovering = auto()
+    #:
+    recovery_success = auto()
+    #:
+    recovery_failed = auto()
+    #:
+    all = auto()
+
+
+class TestPmdMempoolAllocationMode(StrEnum):
+    native = auto()
+    """Create and populate mempool using native DPDK memory."""
+    anon = auto()
+    """Create mempool using native DPDK memory, but populate using anonymous memory."""
+    xmem = auto()
+    """Create and populate mempool using externally and anonymously allocated area."""
+    xmemhuge = auto()
+    """Create and populate mempool using externally and anonymously allocated hugepage area."""
+
+
+@dataclass(kw_only=True)
+class TestPmdTXOnlyForwardingMode(Params):
+    __forward_mode: Literal[TestPmdForwardingModes.txonly] = field(
+        default=TestPmdForwardingModes.txonly, init=False, metadata=long("forward-mode")
+    )
+    multi_flow: Option = field(default=None, metadata=long("txonly-multi-flow"))
+    """Generate multiple flows."""
+    segments_length: XYPair | None = field(default=None, metadata=long("txpkts"))
+    """Set TX segment sizes or total packet length."""
+
+
+@dataclass(kw_only=True)
+class TestPmdFlowGenForwardingMode(Params):
+    __forward_mode: Literal[TestPmdForwardingModes.flowgen] = field(
+        default=TestPmdForwardingModes.flowgen, init=False, metadata=long("forward-mode")
+    )
+    clones: int | None = field(default=None, metadata=long("flowgen-clones"))
+    """Set the number of each packet clones to be sent. Sending clones reduces host CPU load on
+    creating packets and may help in testing extreme speeds or maxing out Tx packet performance.
+    N should be not zero, but less than ‘burst’ parameter.
+    """
+    flows: int | None = field(default=None, metadata=long("flowgen-flows"))
+    """Set the number of flows to be generated, where 1 <= N <= INT32_MAX."""
+    segments_length: XYPair | None = field(default=None, metadata=long("txpkts"))
+    """Set TX segment sizes or total packet length."""
+
+
+@dataclass(kw_only=True)
+class TestPmdNoisyForwardingMode(Params):
+    __forward_mode: Literal[TestPmdForwardingModes.noisy] = field(
+        default=TestPmdForwardingModes.noisy, init=False, metadata=long("forward-mode")
+    )
+    forward_mode: (
+        Literal[
+            TestPmdForwardingModes.io,
+            TestPmdForwardingModes.mac,
+            TestPmdForwardingModes.macswap,
+            TestPmdForwardingModes.fivetswap,
+        ]
+        | None
+    ) = field(default=TestPmdForwardingModes.io, metadata=long("noisy-forward-mode"))
+    """Set the noisy vnf forwarding mode."""
+    tx_sw_buffer_size: int | None = field(default=None, metadata=long("noisy-tx-sw-buffer-size"))
+    """Set the maximum number of elements of the FIFO queue to be created for buffering packets.
+    The default value is 0.
+    """
+    tx_sw_buffer_flushtime: int | None = field(
+        default=None, metadata=long("noisy-tx-sw-buffer-flushtime")
+    )
+    """Set the time before packets in the FIFO queue are flushed. The default value is 0."""
+    lkup_memory: int | None = field(default=None, metadata=long("noisy-lkup-memory"))
+    """Set the size of the noisy neighbor simulation memory buffer in MB to N. The default value is 0."""
+    lkup_num_reads: int | None = field(default=None, metadata=long("noisy-lkup-num-reads"))
+    """Set the number of reads to be done in noisy neighbor simulation memory buffer to N.
+    The default value is 0.
+    """
+    lkup_num_writes: int | None = field(default=None, metadata=long("noisy-lkup-num-writes"))
+    """Set the number of writes to be done in noisy neighbor simulation memory buffer to N.
+    The default value is 0.
+    """
+    lkup_num_reads_writes: int | None = field(
+        default=None, metadata=long("noisy-lkup-num-reads-writes")
+    )
+    """Set the number of r/w accesses to be done in noisy neighbor simulation memory buffer to N.
+    The default value is 0.
+    """
+
+
+@dataclass(kw_only=True)
+class TestPmdAnonMempoolAllocationMode(Params):
+    __mp_alloc: Literal[TestPmdMempoolAllocationMode.anon] = field(
+        default=TestPmdMempoolAllocationMode.anon, init=False, metadata=long("mp-alloc")
+    )
+    no_iova_contig: Option = None
+    """Enable to create mempool which is not IOVA contiguous."""
+
+
+@dataclass(kw_only=True)
+class TestPmdRXRingParams(Params):
+    descriptors: int | None = field(default=None, metadata=long("rxd"))
+    """Set the number of descriptors in the RX rings to N, where N > 0. The default value is 128."""
+    prefetch_threshold: int | None = field(default=None, metadata=long("rxpt"))
+    """Set the prefetch threshold register of RX rings to N, where N >= 0. The default value is 8."""
+    host_threshold: int | None = field(default=None, metadata=long("rxht"))
+    """Set the host threshold register of RX rings to N, where N >= 0. The default value is 8."""
+    write_back_threshold: int | None = field(default=None, metadata=long("rxwt"))
+    """Set the write-back threshold register of RX rings to N, where N >= 0. The default value is 4."""
+    free_threshold: int | None = field(default=None, metadata=long("rxfreet"))
+    """Set the free threshold of RX descriptors to N, where 0 <= N < value of ``-–rxd``.
+    The default value is 0.
+    """
+
+
+@dataclass
+class TestPmdDisableRSS(Params):
+    """Disable RSS (Receive Side Scaling)."""
+
+    __disable_rss: Literal[True] = field(default=True, init=False, metadata=long("disable-rss"))
+
+
+@dataclass
+class TestPmdSetRSSIPOnly(Params):
+    """Set RSS functions for IPv4/IPv6 only."""
+
+    __rss_ip: Literal[True] = field(default=True, init=False, metadata=long("rss-ip"))
+
+
+@dataclass
+class TestPmdSetRSSUDP(Params):
+    """Set RSS functions for IPv4/IPv6 and UDP."""
+
+    __rss_udp: Literal[True] = field(default=True, init=False, metadata=long("rss-udp"))
+
+
+@dataclass(kw_only=True)
+class TestPmdTXRingParams(Params):
+    descriptors: int | None = field(default=None, metadata=long("txd"))
+    """Set the number of descriptors in the TX rings to N, where N > 0. The default value is 512."""
+    rs_bit_threshold: int | None = field(default=None, metadata=long("txrst"))
+    """Set the transmit RS bit threshold of TX rings to N, where 0 <= N <= value of ``--txd``.
+    The default value is 0.
+    """
+    prefetch_threshold: int | None = field(default=None, metadata=long("txpt"))
+    """Set the prefetch threshold register of TX rings to N, where N >= 0. The default value is 36."""
+    host_threshold: int | None = field(default=None, metadata=long("txht"))
+    """Set the host threshold register of TX rings to N, where N >= 0. The default value is 0."""
+    write_back_threshold: int | None = field(default=None, metadata=long("txwt"))
+    """Set the write-back threshold register of TX rings to N, where N >= 0. The default value is 0."""
+    free_threshold: int | None = field(default=None, metadata=long("txfreet"))
+    """Set the transmit free threshold of TX rings to N, where 0 <= N <= value of ``--txd``.
+    The default value is 0.
+    """
+
+
+@dataclass(slots=True, kw_only=True)
+class TestPmdParameters(Params):
+    """The testpmd shell parameters.
+
+    The string representation can be created by converting the instance to a string.
+    """
+
+    interactive_mode: Option = field(default=True, metadata=short("i"))
+    """Runs testpmd in interactive mode."""
+    auto_start: Option = field(default=None, metadata=short("a"))
+    """Start forwarding on initialization."""
+    tx_first: Option = None
+    """Start forwarding, after sending a burst of packets first."""
+
+    stats_period: int | None = None
+    """Display statistics every ``PERIOD`` seconds, if interactive mode is disabled.
+    The default value is 0, which means that the statistics will not be displayed.
+
+    .. note:: This flag should be used only in non-interactive mode.
+    """
+
+    display_xstats: list[str] | None = field(default=None, metadata=field_mixins(comma_separated))
+    """Display comma-separated list of extended statistics every ``PERIOD`` seconds as specified in
+    ``--stats-period`` or when used with interactive commands that show Rx/Tx statistics
+    (i.e. ‘show port stats’).
+    """
+
+    nb_cores: int | None = 1
+    """Set the number of forwarding cores, where 1 <= N <= “number of cores” or ``RTE_MAX_LCORE``
+    from the configuration file. The default value is 1.
+    """
+    coremask: int | None = field(default=None, metadata=field_mixins(hex))
+    """Set the hexadecimal bitmask of the cores running the packet forwarding test. The main lcore
+    is reserved for command line parsing only and cannot be masked on for packet forwarding.
+    """
+
+    nb_ports: int | None = None
+    """Set the number of forwarding ports, where 1 <= N <= “number of ports” on the board or
+    ``RTE_MAX_ETHPORTS`` from the configuration file. The default value is the number of ports
+    on the board.
+    """
+    port_topology: TestPmdPortTopology | None = TestPmdPortTopology.paired
+    """Set port topology, where mode is paired (the default), chained or loop."""
+    portmask: int | None = field(default=None, metadata=field_mixins(hex))
+    """Set the hexadecimal bitmask of the ports used by the packet forwarding test."""
+    portlist: str | None = None  # TODO: can be ranges 0,1-3
+    """Set the forwarding ports based on the user input used by the packet forwarding test.
+    ‘-‘ denotes a range of ports to set including the two specified port IDs ‘,’ separates
+    multiple port values. Possible examples like –portlist=0,1 or –portlist=0-2 or –portlist=0,1-2 etc
+    """
+
+    numa: BooleanOption = True
+    """Enable/disable NUMA-aware allocation of RX/TX rings and of RX memory buffers (mbufs). Enabled by default."""
+    socket_num: int | None = None
+    """Set the socket from which all memory is allocated in NUMA mode, where 0 <= N < number of sockets on the board."""
+    port_numa_config: list[TestPmdPortNUMAConfig] | None = field(
+        default=None, metadata=field_mixins(comma_separated)
+    )
+    """Specify the socket on which the memory pool to be used by the port will be allocated."""
+    ring_numa_config: list[TestPmdRingNUMAConfig] | None = field(
+        default=None, metadata=field_mixins(comma_separated)
+    )
+    """Specify the socket on which the TX/RX rings for the port will be allocated.
+    Where flag is 1 for RX, 2 for TX, and 3 for RX and TX.
+    """
+
+    # Mbufs
+    total_num_mbufs: int | None = None
+    """Set the number of mbufs to be allocated in the mbuf pools, where N > 1024."""
+    mbuf_size: list[int] | None = field(default=None, metadata=field_mixins(comma_separated))
+    """Set the data size of the mbufs used to N bytes, where N < 65536. The default value is 2048.
+    If multiple mbuf-size values are specified the extra memory pools will be created for
+    allocating mbufs to receive packets with buffer splitting features.
+    """
+    mbcache: int | None = None
+    """Set the cache of mbuf memory pools to N, where 0 <= N <= 512. The default value is 16."""
+
+    max_pkt_len: int | None = None
+    """Set the maximum packet size to N bytes, where N >= 64. The default value is 1518."""
+
+    eth_peers_configfile: PurePath | None = None
+    """Use a configuration file containing the Ethernet addresses of the peer ports."""
+    eth_peer: list[TestPmdEthPeer] | None = field(default=None, metadata=multiple())
+    """Set the MAC address XX:XX:XX:XX:XX:XX of the peer port N, where 0 <= N < RTE_MAX_ETHPORTS."""
+
+    tx_ip: TestPmdTxIPAddrPair | None = TestPmdTxIPAddrPair(
+        source_ip="198.18.0.1", dest_ip="198.18.0.2"
+    )
+    """Set the source and destination IP address used when doing transmit only test.
+    The defaults address values are source 198.18.0.1 and destination 198.18.0.2.
+    These are special purpose addresses reserved for benchmarking (RFC 5735).
+    """
+    tx_udp: TestPmdTxUDPPortPair | None = TestPmdTxUDPPortPair(9)
+    """Set the source and destination UDP port number for transmit test only test.
+    The default port is the port 9 which is defined for the discard protocol (RFC 863)."""
+
+    enable_lro: Option = None
+    """Enable large receive offload."""
+    max_lro_pkt_size: int | None = None
+    """Set the maximum LRO aggregated packet size to N bytes, where N >= 64."""
+
+    disable_crc_strip: Option = None
+    """Disable hardware CRC stripping."""
+    enable_scatter: Option = None
+    """Enable scatter (multi-segment) RX."""
+    enable_hw_vlan: Option = None
+    """Enable hardware VLAN."""
+    enable_hw_vlan_filter: Option = None
+    """Enable hardware VLAN filter."""
+    enable_hw_vlan_strip: Option = None
+    """Enable hardware VLAN strip."""
+    enable_hw_vlan_extend: Option = None
+    """Enable hardware VLAN extend."""
+    enable_hw_qinq_strip: Option = None
+    """Enable hardware QINQ strip."""
+    pkt_drop_enabled: Option = field(default=None, metadata=long("enable-drop-en"))
+    """Enable per-queue packet drop for packets with no descriptors."""
+
+    rss: TestPmdDisableRSS | TestPmdSetRSSIPOnly | TestPmdSetRSSUDP | None = None
+    """RSS option setting.
+
+    The value can be one of:
+    * :class:`TestPmdDisableRSS`, to disable RSS
+    * :class:`TestPmdSetRSSIPOnly`, to set RSS for IPv4/IPv6 only
+    * :class:`TestPmdSetRSSUDP`, to set RSS for IPv4/IPv6 and UDP
+    """
+
+    forward_mode: (
+        Literal[
+            TestPmdForwardingModes.io,
+            TestPmdForwardingModes.mac,
+            TestPmdForwardingModes.macswap,
+            TestPmdForwardingModes.rxonly,
+            TestPmdForwardingModes.csum,
+            TestPmdForwardingModes.icmpecho,
+            TestPmdForwardingModes.ieee1588,
+            TestPmdForwardingModes.fivetswap,
+            TestPmdForwardingModes.shared_rxq,
+            TestPmdForwardingModes.recycle_mbufs,
+        ]
+        | TestPmdFlowGenForwardingMode
+        | TestPmdTXOnlyForwardingMode
+        | TestPmdNoisyForwardingMode
+        | None
+    ) = TestPmdForwardingModes.io
+    """Set the forwarding mode.
+
+    The value can be one of:
+    * :attr:`TestPmdForwardingModes.io` (default)
+    * :attr:`TestPmdForwardingModes.mac`
+    * :attr:`TestPmdForwardingModes.rxonly`
+    * :attr:`TestPmdForwardingModes.csum`
+    * :attr:`TestPmdForwardingModes.icmpecho`
+    * :attr:`TestPmdForwardingModes.ieee1588`
+    * :attr:`TestPmdForwardingModes.fivetswap`
+    * :attr:`TestPmdForwardingModes.shared_rxq`
+    * :attr:`TestPmdForwardingModes.recycle_mbufs`
+    * :class:`FlowGenForwardingMode`
+    * :class:`TXOnlyForwardingMode`
+    * :class:`NoisyForwardingMode`
+    """
+
+    hairpin_mode: TestPmdHairpinMode | None = TestPmdHairpinMode(0)
+    """Set the hairpin port configuration."""
+    hairpin_queues: int | None = field(default=None, metadata=long("hairpinq"))
+    """Set the number of hairpin queues per port to N, where 1 <= N <= 65535. The default value is 0."""
+
+    burst: int | None = None
+    """Set the number of packets per burst to N, where 1 <= N <= 512. The default value is 32.
+    If set to 0, driver default is used if defined.
+    Else, if driver default is not defined, default of 32 is used.
+    """
+
+    # RX data parameters
+    enable_rx_cksum: Option = None
+    """Enable hardware RX checksum offload."""
+    rx_queues: int | None = field(default=None, metadata=long("rxq"))
+    """Set the number of RX queues per port to N, where 1 <= N <= 65535. The default value is 1."""
+    rx_ring: TestPmdRXRingParams | None = None
+    """Set the RX rings parameters."""
+    no_flush_rx: Option = None
+    """Don’t flush the RX streams before starting forwarding. Used mainly with the PCAP PMD."""
+    rx_segments_offsets: XYPair | None = field(default=None, metadata=long("rxoffs"))
+    """Set the offsets of packet segments on receiving if split feature is engaged.
+    Affects only the queues configured with split offloads (currently BUFFER_SPLIT is supported only).
+    """
+    rx_segments_length: XYPair | None = field(default=None, metadata=long("rxpkts"))
+    """Set the length of segments to scatter packets on receiving if split feature is engaged.
+    Affects only the queues configured with split offloads (currently BUFFER_SPLIT is supported only).
+    Optionally the multiple memory pools can be specified with –mbuf-size command line parameter and
+    the mbufs to receive will be allocated sequentially from these extra memory pools.
+    """
+    multi_rx_mempool: Option = None
+    """Enable multiple mbuf pools per Rx queue."""
+    rx_shared_queue: Option | int = field(default=None, metadata=long("rxq-share"))
+    """Create queues in shared Rx queue mode if device supports. Shared Rx queues are grouped per X ports.
+    X defaults to UINT32_MAX, implies all ports join share group 1.
+    Forwarding engine “shared-rxq” should be used for shared Rx queues.
+    This engine does Rx only and update stream statistics accordingly.
+    """
+    rx_offloads: int | None = field(default=0, metadata=field_mixins(hex))
+    """Set the hexadecimal bitmask of RX queue offloads. The default value is 0."""
+    rx_mq_mode: TestPmdRXMultiQueueMode | None = (
+        TestPmdRXMultiQueueMode.DCB | TestPmdRXMultiQueueMode.RSS | TestPmdRXMultiQueueMode.VMDQ
+    )
+    """Set the hexadecimal bitmask of RX multi queue mode which can be enabled."""
+
+    # TX data parameters
+    tx_queues: int | None = field(default=None, metadata=long("txq"))
+    """Set the number of TX queues per port to N, where 1 <= N <= 65535. The default value is 1."""
+    tx_ring: TestPmdTXRingParams | None = None
+    """Set the TX rings params."""
+    tx_offloads: int | None = field(default=0, metadata=field_mixins(hex))
+    """Set the hexadecimal bitmask of TX queue offloads. The default value is 0."""
+
+    eth_link_speed: int | None = None
+    """Set a forced link speed to the ethernet port. E.g. 1000 for 1Gbps."""
+    disable_link_check: Option = None
+    """Disable check on link status when starting/stopping ports."""
+    disable_device_start: Option = None
+    """Do not automatically start all ports.
+    This allows testing configuration of rx and tx queues before device is started for the first time.
+    """
+    no_lsc_interrupt: Option = None
+    """Disable LSC interrupts for all ports, even those supporting it."""
+    no_rmv_interrupt: Option = None
+    """Disable RMV interrupts for all ports, even those supporting it."""
+    bitrate_stats: int | None = None
+    """Set the logical core N to perform bitrate calculation."""
+    latencystats: int | None = None
+    """Set the logical core N to perform latency and jitter calculations."""
+    print_events: list[TestPmdEvent] | None = field(
+        default=None, metadata=multiple(long("print-event"))
+    )
+    """Enable printing the occurrence of the designated events.
+    Using :attr:`TestPmdEvent.ALL` will enable all of them.
+    """
+    mask_events: list[TestPmdEvent] | None = field(
+        default_factory=lambda: [TestPmdEvent.intr_lsc], metadata=multiple(long("mask-event"))
+    )
+    """Disable printing the occurrence of the designated events.
+    Using :attr:`TestPmdEvent.ALL` will disable all of them.
+    """
+
+    flow_isolate_all: Option = None
+    """Providing this parameter requests flow API isolated mode on all ports at initialization time.
+    It ensures all traffic is received through the configured flow rules only (see flow command).
+
+    Ports that do not support this mode are automatically discarded.
+    """
+    disable_flow_flush: Option = None
+    """Disable port flow flush when stopping port.
+    This allows testing keep flow rules or shared flow objects across restart.
+    """
+
+    hot_plug: Option = None
+    """Enable device event monitor mechanism for hotplug."""
+    vxlan_gpe_port: int | None = None
+    """Set the UDP port number of tunnel VXLAN-GPE to N. The default value is 4790."""
+    geneve_parsed_port: int | None = None
+    """Set the UDP port number that is used for parsing the GENEVE protocol to N.
+    HW may be configured with another tunnel Geneve port. The default value is 6081.
+    """
+    lock_all_memory: BooleanOption = field(default=False, metadata=long("mlockall"))
+    """Enable/disable locking all memory. Disabled by default."""
+    mempool_allocation_mode: (
+        Literal[
+            TestPmdMempoolAllocationMode.native,
+            TestPmdMempoolAllocationMode.xmem,
+            TestPmdMempoolAllocationMode.xmemhuge,
+        ]
+        | TestPmdAnonMempoolAllocationMode
+        | None
+    ) = field(default=None, metadata=long("mp-alloc"))
+    """Select mempool allocation mode.
+
+    The value can be one of:
+    * :attr:`TestPmdMempoolAllocationMode.native`
+    * :class:`TestPmdAnonMempoolAllocationMode`
+    * :attr:`TestPmdMempoolAllocationMode.xmem`
+    * :attr:`TestPmdMempoolAllocationMode.xmemhuge`
+    """
+    record_core_cycles: Option = None
+    """Enable measurement of CPU cycles per packet."""
+    record_burst_status: Option = None
+    """Enable display of RX and TX burst stats."""
+
+
+class TestPmdDevice(object):
+    """The data of a device that testpmd can recognize.
+
+    Attributes:
+        pci_address: The PCI address of the device.
+    """
+
+    pci_address: str
+
+    def __init__(self, pci_address_line: str):
+        """Initialize the device from the testpmd output line string.
+
+        Args:
+            pci_address_line: A line of testpmd output that contains a device.
+        """
+        self.pci_address = pci_address_line.strip().split(": ")[1].strip()
+
+    def __str__(self) -> str:
+        """The PCI address captures what the device is."""
+        return self.pci_address
+
+
 class TestPmdShell(InteractiveShell):
     """Testpmd interactive shell.
 
@@ -123,8 +720,8 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
 
         assert isinstance(self._app_args, EalParameters)
 
-        if isinstance(self._app_args.app_params, StrParams):
-            self._app_args.app_params.value += " -i --mask-event intr_lsc"
+        if self._app_args.app_params is None:
+            self._app_args.app_params = TestPmdParameters()
 
         self.number_of_ports = len(self._app_args.ports) if self._app_args.ports is not None else 0
 
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH 4/6] dts: use testpmd params for scatter test suite
  2024-03-26 19:04 [PATCH 0/6] dts: add testpmd params and statefulness Luca Vizzarro
                   ` (2 preceding siblings ...)
  2024-03-26 19:04 ` [PATCH 3/6] dts: add testpmd shell params Luca Vizzarro
@ 2024-03-26 19:04 ` Luca Vizzarro
  2024-04-09 19:12   ` Juraj Linkeš
  2024-03-26 19:04 ` [PATCH 5/6] dts: add statefulness to InteractiveShell Luca Vizzarro
                   ` (7 subsequent siblings)
  11 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-03-26 19:04 UTC (permalink / raw)
  To: dev
  Cc: Juraj Linkeš,
	Luca Vizzarro, Jack Bond-Preston, Honnappa Nagarahalli

Update the buffer scatter test suite to use TestPmdParameters
instead of the StrParams implementation.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Jack Bond-Preston <jack.bond-preston@arm.com>
Reviewed-by: Honnappa Nagarahalli <honnappa.nagarahalli@arm.com>
---
 dts/tests/TestSuite_pmd_buffer_scatter.py | 19 +++++++++++--------
 1 file changed, 11 insertions(+), 8 deletions(-)

diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index 4cdbdc4272..c6d313fc64 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -23,7 +23,11 @@
 from scapy.utils import hexstr  # type: ignore[import]
 
 from framework.params import StrParams
-from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
+from framework.remote_session.testpmd_shell import (
+    TestPmdForwardingModes,
+    TestPmdShell,
+    TestPmdParameters,
+)
 from framework.test_suite import TestSuite
 
 
@@ -104,16 +108,15 @@ def pmd_scatter(self, mbsize: int) -> None:
         """
         testpmd = self.sut_node.create_interactive_shell(
             TestPmdShell,
-            app_parameters=StrParams(
-                "--mbcache=200 "
-                f"--mbuf-size={mbsize} "
-                "--max-pkt-len=9000 "
-                "--port-topology=paired "
-                "--tx-offloads=0x00008000"
+            app_parameters=TestPmdParameters(
+                forward_mode=TestPmdForwardingModes.mac,
+                mbcache=200,
+                mbuf_size=[mbsize],
+                max_pkt_len=9000,
+                tx_offloads=0x00008000,
             ),
             privileged=True,
         )
-        testpmd.set_forward_mode(TestPmdForwardingModes.mac)
         testpmd.start()
 
         for offset in [-1, 0, 1, 4, 5]:
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH 5/6] dts: add statefulness to InteractiveShell
  2024-03-26 19:04 [PATCH 0/6] dts: add testpmd params and statefulness Luca Vizzarro
                   ` (3 preceding siblings ...)
  2024-03-26 19:04 ` [PATCH 4/6] dts: use testpmd params for scatter test suite Luca Vizzarro
@ 2024-03-26 19:04 ` Luca Vizzarro
  2024-03-28 16:48   ` Jeremy Spewock
  2024-03-26 19:04 ` [PATCH 6/6] dts: add statefulness to TestPmdShell Luca Vizzarro
                   ` (6 subsequent siblings)
  11 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-03-26 19:04 UTC (permalink / raw)
  To: dev
  Cc: Juraj Linkeš,
	Luca Vizzarro, Jack Bond-Preston, Honnappa Nagarahalli

The InteractiveShell class can be started in privileged mode, but this
is not saved for reference to the tests developer. Moreover, originally
a command timeout could only be set at initialisation, this can now be
amended and reset back as needed.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Jack Bond-Preston <jack.bond-preston@arm.com>
Reviewed-by: Honnappa Nagarahalli <honnappa.nagarahalli@arm.com>
---
 .../remote_session/interactive_shell.py        | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)

diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
index a2c7b30d9f..5d80061e8d 100644
--- a/dts/framework/remote_session/interactive_shell.py
+++ b/dts/framework/remote_session/interactive_shell.py
@@ -41,8 +41,10 @@ class InteractiveShell(ABC):
     _stdout: channel.ChannelFile
     _ssh_channel: Channel
     _logger: DTSLogger
+    __default_timeout: float
     _timeout: float
     _app_args: Params | None
+    _is_privileged: bool = False
 
     #: Prompt to expect at the end of output when sending a command.
     #: This is often overridden by subclasses.
@@ -88,7 +90,7 @@ def __init__(
         self._ssh_channel.settimeout(timeout)
         self._ssh_channel.set_combine_stderr(True)  # combines stdout and stderr streams
         self._logger = logger
-        self._timeout = timeout
+        self._timeout = self.__default_timeout = timeout
         self._app_args = app_args
         self._start_application(get_privileged_command)
 
@@ -105,6 +107,7 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
         start_command = f"{self.path} {self._app_args or ''}"
         if get_privileged_command is not None:
             start_command = get_privileged_command(start_command)
+            self._is_privileged = True
         self.send_command(start_command)
 
     def send_command(self, command: str, prompt: str | None = None) -> str:
@@ -150,3 +153,16 @@ def close(self) -> None:
     def __del__(self) -> None:
         """Make sure the session is properly closed before deleting the object."""
         self.close()
+
+    @property
+    def is_privileged(self) -> bool:
+        """Property specifying if the interactive shell is running in privileged mode."""
+        return self._is_privileged
+
+    def set_timeout(self, timeout: float):
+        """Set the timeout to use with the next commands."""
+        self._timeout = timeout
+
+    def reset_timeout(self):
+        """Reset the timeout to the default setting."""
+        self._timeout = self.__default_timeout
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH 6/6] dts: add statefulness to TestPmdShell
  2024-03-26 19:04 [PATCH 0/6] dts: add testpmd params and statefulness Luca Vizzarro
                   ` (4 preceding siblings ...)
  2024-03-26 19:04 ` [PATCH 5/6] dts: add statefulness to InteractiveShell Luca Vizzarro
@ 2024-03-26 19:04 ` Luca Vizzarro
  2024-03-28 16:48   ` Jeremy Spewock
  2024-04-10  7:50   ` Juraj Linkeš
  2024-05-09 11:20 ` [PATCH v2 0/8] dts: add testpmd params Luca Vizzarro
                   ` (5 subsequent siblings)
  11 siblings, 2 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-03-26 19:04 UTC (permalink / raw)
  To: dev
  Cc: Juraj Linkeš,
	Luca Vizzarro, Jack Bond-Preston, Honnappa Nagarahalli

This commit provides a state container for TestPmdShell. It currently
only indicates whether the packet forwarding has started
or not, and the number of ports which were given to the shell.

This also fixes the behaviour of `wait_link_status_up` to use the
command timeout as inherited from InteractiveShell.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Jack Bond-Preston <jack.bond-preston@arm.com>
Reviewed-by: Honnappa Nagarahalli <honnappa.nagarahalli@arm.com>
---
 dts/framework/remote_session/testpmd_shell.py | 41 +++++++++++++------
 1 file changed, 28 insertions(+), 13 deletions(-)

diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index a823dc53be..ea1d254f86 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -678,19 +678,27 @@ def __str__(self) -> str:
         return self.pci_address
 
 
+@dataclass(slots=True)
+class TestPmdState:
+    """Session state container."""
+
+    #:
+    packet_forwarding_started: bool = False
+
+    #: The number of ports which were allowed on the command-line when testpmd was started.
+    number_of_ports: int = 0
+
+
 class TestPmdShell(InteractiveShell):
     """Testpmd interactive shell.
 
     The testpmd shell users should never use
     the :meth:`~.interactive_shell.InteractiveShell.send_command` method directly, but rather
     call specialized methods. If there isn't one that satisfies a need, it should be added.
-
-    Attributes:
-        number_of_ports: The number of ports which were allowed on the command-line when testpmd
-            was started.
     """
 
-    number_of_ports: int
+    #: Current state
+    state: TestPmdState = TestPmdState()
 
     #: The path to the testpmd executable.
     path: ClassVar[PurePath] = PurePath("app", "dpdk-testpmd")
@@ -723,7 +731,13 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
         if self._app_args.app_params is None:
             self._app_args.app_params = TestPmdParameters()
 
-        self.number_of_ports = len(self._app_args.ports) if self._app_args.ports is not None else 0
+        assert isinstance(self._app_args.app_params, TestPmdParameters)
+
+        if self._app_args.app_params.auto_start:
+            self.state.packet_forwarding_started = True
+
+        if self._app_args.ports is not None:
+            self.state.number_of_ports = len(self._app_args.ports)
 
         super()._start_application(get_privileged_command)
 
@@ -746,12 +760,14 @@ def start(self, verify: bool = True) -> None:
                 self._logger.debug(f"Failed to start packet forwarding: \n{start_cmd_output}")
                 raise InteractiveCommandExecutionError("Testpmd failed to start packet forwarding.")
 
-            for port_id in range(self.number_of_ports):
+            for port_id in range(self.state.number_of_ports):
                 if not self.wait_link_status_up(port_id):
                     raise InteractiveCommandExecutionError(
                         "Not all ports came up after starting packet forwarding in testpmd."
                     )
 
+        self.state.packet_forwarding_started = True
+
     def stop(self, verify: bool = True) -> None:
         """Stop packet forwarding.
 
@@ -773,6 +789,8 @@ def stop(self, verify: bool = True) -> None:
                 self._logger.debug(f"Failed to stop packet forwarding: \n{stop_cmd_output}")
                 raise InteractiveCommandExecutionError("Testpmd failed to stop packet forwarding.")
 
+        self.state.packet_forwarding_started = False
+
     def get_devices(self) -> list[TestPmdDevice]:
         """Get a list of device names that are known to testpmd.
 
@@ -788,19 +806,16 @@ def get_devices(self) -> list[TestPmdDevice]:
                 dev_list.append(TestPmdDevice(line))
         return dev_list
 
-    def wait_link_status_up(self, port_id: int, timeout=SETTINGS.timeout) -> bool:
-        """Wait until the link status on the given port is "up".
+    def wait_link_status_up(self, port_id: int) -> bool:
+        """Wait until the link status on the given port is "up". Times out.
 
         Arguments:
             port_id: Port to check the link status on.
-            timeout: Time to wait for the link to come up. The default value for this
-                argument may be modified using the :option:`--timeout` command-line argument
-                or the :envvar:`DTS_TIMEOUT` environment variable.
 
         Returns:
             Whether the link came up in time or not.
         """
-        time_to_stop = time.time() + timeout
+        time_to_stop = time.time() + self._timeout
         port_info: str = ""
         while time.time() < time_to_stop:
             port_info = self.send_command(f"show port info {port_id}")
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 1/6] dts: add parameters data structure
  2024-03-26 19:04 ` [PATCH 1/6] dts: add parameters data structure Luca Vizzarro
@ 2024-03-28 16:48   ` Jeremy Spewock
  2024-04-09 15:52     ` Luca Vizzarro
  2024-04-09 12:10   ` Juraj Linkeš
  1 sibling, 1 reply; 159+ messages in thread
From: Jeremy Spewock @ 2024-03-28 16:48 UTC (permalink / raw)
  To: Luca Vizzarro
  Cc: dev, Juraj Linkeš, Jack Bond-Preston, Honnappa Nagarahalli

Overall I like the idea of having a structured way of passing
command-line arguments to applications as strings and I think that
this is a well-abstracted approach. I also like that this approach
still supports the ability to pass strings "as-is" and use them as
parameters as well. That opens the door for potentially creating
dataclasses which only detail key-parameters that we assume you will
use, without blocking you from inputting whatever you want.

On Tue, Mar 26, 2024 at 3:04 PM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
<snip>
> +META_VALUE_ONLY = "value_only"
> +META_OPTIONS_END = "options_end"
> +META_SHORT_NAME = "short_name"
> +META_LONG_NAME = "long_name"
> +META_MULTIPLE = "multiple"
> +META_MIXINS = "mixins"
> +
> +

I might add some kind of block comment here as a separator that
delimits that metadata modifiers start here. Something like what is
written in scapy.py which creates sections for XML-RPC method vs ones
that are run by the docker container. This isn't something strictly
necessary, but it might help break things up and add a little more
explanation.

> +def value_only(metadata: dict[str, Any] = {}) -> dict[str, Any]:
> +    """Injects the value of the attribute as-is without flag. Metadata modifier for :func:`dataclasses.field`."""
> +    return {**metadata, META_VALUE_ONLY: True}
> +
> +
<snip>

You could do the same thing here for mixins, but again, I'm not sure
it's really necessary.

> +def field_mixins(*mixins: Mixin, metadata: dict[str, Any] = {}) -> dict[str, Any]:
> +    """Takes in a variable number of mixins to manipulate the value's rendering. Metadata modifier for :func:`dataclasses.field`.
> +
> +    The ``metadata`` keyword argument can be used to chain metadata modifiers together.
> +
> +    Mixins can be chained together, executed from right to left in the arguments list order.
> +
> +    Example:
> +
> +    .. code:: python
> +
> +        hex_bitmask: int | None = field(default=0b1101, metadata=field_mixins(hex, metadata=param_name("mask")))
> +
> +    will render as ``--mask=0xd``. The :func:`hex` built-in can be used as a mixin turning a valid integer into a hexadecimal representation.
> +    """
> +    return {**metadata, META_MIXINS: mixins}
<snip>
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 2/6] dts: use Params for interactive shells
  2024-03-26 19:04 ` [PATCH 2/6] dts: use Params for interactive shells Luca Vizzarro
@ 2024-03-28 16:48   ` Jeremy Spewock
  2024-04-09 14:56     ` Juraj Linkeš
  2024-05-28 15:43   ` Nicholas Pratte
  1 sibling, 1 reply; 159+ messages in thread
From: Jeremy Spewock @ 2024-03-28 16:48 UTC (permalink / raw)
  To: Luca Vizzarro
  Cc: dev, Juraj Linkeš, Jack Bond-Preston, Honnappa Nagarahalli

On Tue, Mar 26, 2024 at 3:04 PM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Make it so that interactive shells accept an implementation of `Params`
> for app arguments. Convert EalParameters to use `Params` instead.
>
> String command line parameters can still be supplied by using the
> `StrParams` implementation.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Jack Bond-Preston <jack.bond-preston@arm.com>
> Reviewed-by: Honnappa Nagarahalli <honnappa.nagarahalli@arm.com>
> ---
<snip>
> @@ -40,7 +42,7 @@ class InteractiveShell(ABC):
>      _ssh_channel: Channel
>      _logger: DTSLogger
>      _timeout: float
> -    _app_args: str
> +    _app_args: Params | None
>

I'm not sure if allowing None should be the solution for these shells
as opposed to just supplying an empty parameter object. Maybe
something that could be done is the factory method in sut_node allows
it to be None, but when it comes time to make the abstract shell it
creates an empty one if it doesn't exist, or the init method makes
this an optional parameter but creates it if it doesn't exist.

I suppose this logic would have to exist somewhere because the
parameters aren't always required, it's just a question of where we
should put it and if we should just assume that the interactive shell
class just knows how to accept some parameters and put them into the
shell. I would maybe leave this as something that cannot be None and
handle it outside of the shell, but I'm not sure it's something really
required and I don't have a super strong opinion on it.

>      #: Prompt to expect at the end of output when sending a command.
>      #: This is often overridden by subclasses.
<snip>
> @@ -118,8 +119,15 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
>          Also find the number of pci addresses which were allowed on the command line when the app
>          was started.
>          """
> -        self._app_args += " -i --mask-event intr_lsc"
> -        self.number_of_ports = self._app_args.count("-a ")
> +        from framework.testbed_model.sut_node import EalParameters
> +
> +        assert isinstance(self._app_args, EalParameters)
> +
> +        if isinstance(self._app_args.app_params, StrParams):
> +            self._app_args.app_params.value += " -i --mask-event intr_lsc"
> +
> +        self.number_of_ports = len(self._app_args.ports) if self._app_args.ports is not None else 0

I we should override the _app_args parameter in the testpmd shell to
always be EalParameters instead of doing this import and assertion.
It's a DPDK app, so we will always need EalParameters anyway, so we
might as well put that as our typehint to start off as what we expect
instead.

The checking of an instance of StrParams also feels a little strange
here, it might be more ideal if we could just add the parameters
without this check. Maybe something we can do, just because these
parameters are meant to be CLI commands anyway and will be rendered as
a string, is replace the StrParams object with a method on the base
Params dataclass that allows you to just add any string as a value
only field. Then, we don't have to bother validating anything about
the app parameters and we don't care what they are, we can just add a
string to them of new parameters.

I think this is something that likely also gets solved when you
replace this with testpmd parameters, but it might be a good way to
make the Params object more versatile in general so that people can
diverge and add their own flags to it if needed.

> +
>          super()._start_application(get_privileged_command)
>
>      def start(self, verify: bool = True) -> None:
 <snip>
> @@ -134,7 +136,7 @@ def create_interactive_shell(
>          shell_cls: Type[InteractiveShellType],
>          timeout: float,
>          privileged: bool,
> -        app_args: str,
> +        app_args: Params | None,

This also falls in line with what I was saying before about where this
logic is handled. This was made to always be a string originally
because by this point it being optional was already handled by the
sut_node.create_interactive_shell() and we should have some kind of
value here (even if that value is an empty parameters dataclass) to
pass into the application. It sort of semantically doesn't really
change much, but at this point it might as well not be None, so we can
simplify this type.

>      ) -> InteractiveShellType:
>          """Factory for interactive session handlers.
>
<snip>
> +@dataclass(kw_only=True)
> +class EalParameters(Params):
>      """The environment abstraction layer parameters.
>
>      The string representation can be created by converting the instance to a string.
>      """
>
> -    def __init__(
> -        self,
> -        lcore_list: LogicalCoreList,
> -        memory_channels: int,
> -        prefix: str,
> -        no_pci: bool,
> -        vdevs: list[VirtualDevice],
> -        ports: list[Port],
> -        other_eal_param: str,
> -    ):
> -        """Initialize the parameters according to inputs.
> -
> -        Process the parameters into the format used on the command line.
> +    lcore_list: LogicalCoreList = field(metadata=params.short("l"))
> +    """The list of logical cores to use."""
>
> -        Args:
> -            lcore_list: The list of logical cores to use.
> -            memory_channels: The number of memory channels to use.
> -            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
> -            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
> -            vdevs: Virtual devices, e.g.::
> +    memory_channels: int = field(metadata=params.short("n"))
> +    """The number of memory channels to use."""
>
> -                vdevs=[
> -                    VirtualDevice('net_ring0'),
> -                    VirtualDevice('net_ring1')
> -                ]
> -            ports: The list of ports to allow.
> -            other_eal_param: user defined DPDK EAL parameters, e.g.:
> -                ``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._ports = " ".join(f"-a {port.pci}" for port in ports)
> -        self._other_eal_param = other_eal_param
> -
> -    def __str__(self) -> str:
> -        """Create the EAL string."""
> -        return (
> -            f"{self._lcore_list} "
> -            f"{self._memory_channels} "
> -            f"{self._prefix} "
> -            f"{self._no_pci} "
> -            f"{self._vdevs} "
> -            f"{self._ports} "
> -            f"{self._other_eal_param}"
> -        )
> +    prefix: str = field(metadata=params.long("file-prefix"))
> +    """Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``."""

Just a small note on docstrings, I believe generally in DTS we use the
google docstring
(https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
format where it applies with some small differences. Because these
attributes aren't class vars however, I believe they should be in the
docstring for the class in the `Attributes` section. I generally have
trouble remembering exactly how it should look, but Juraj documented
it in `doc/guides/tools/dts.rst`

> +
> +    no_pci: params.Option
> +    """Switch to disable PCI bus e.g.: ``no_pci=True``."""
<snip>

> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 3/6] dts: add testpmd shell params
  2024-03-26 19:04 ` [PATCH 3/6] dts: add testpmd shell params Luca Vizzarro
@ 2024-03-28 16:48   ` Jeremy Spewock
  2024-04-09 16:37   ` Juraj Linkeš
  1 sibling, 0 replies; 159+ messages in thread
From: Jeremy Spewock @ 2024-03-28 16:48 UTC (permalink / raw)
  To: Luca Vizzarro
  Cc: dev, Juraj Linkeš, Jack Bond-Preston, Honnappa Nagarahalli

We talked about this in DTS meeting, looking at this some more, we
already use default parameters for Eal and structure those, so we
already have sort of tied ourselves into a situation of if those ever
change (unlikely) we would need to change as well, so maybe this could
be something we use, I'd like to hear more of peoples thoughts on this
and what Juraj thinks when he is back.

Just because this is fairly large and bloats the testpmd file a little
bit, it might be more worth it to move this into a separate file and
import it so this file doesn't get too large. Especially because this
file will likely already grow quite a bit just from the amount of
testpmd commands we are going to have to handle in the future.

On Tue, Mar 26, 2024 at 3:04 PM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Implement all the testpmd shell parameters into a data structure.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Jack Bond-Preston <jack.bond-preston@arm.com>
> Reviewed-by: Honnappa Nagarahalli <honnappa.nagarahalli@arm.com>
> ---
<snip>
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 5/6] dts: add statefulness to InteractiveShell
  2024-03-26 19:04 ` [PATCH 5/6] dts: add statefulness to InteractiveShell Luca Vizzarro
@ 2024-03-28 16:48   ` Jeremy Spewock
  2024-04-10  6:53     ` Juraj Linkeš
  0 siblings, 1 reply; 159+ messages in thread
From: Jeremy Spewock @ 2024-03-28 16:48 UTC (permalink / raw)
  To: Luca Vizzarro
  Cc: dev, Juraj Linkeš, Jack Bond-Preston, Honnappa Nagarahalli

On Tue, Mar 26, 2024 at 3:04 PM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
<snip>
> diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
> index a2c7b30d9f..5d80061e8d 100644
> --- a/dts/framework/remote_session/interactive_shell.py
> +++ b/dts/framework/remote_session/interactive_shell.py
> @@ -41,8 +41,10 @@ class InteractiveShell(ABC):
>      _stdout: channel.ChannelFile
>      _ssh_channel: Channel
>      _logger: DTSLogger
> +    __default_timeout: float

Only single underscores are used for other private variables, probably
better to keep that consistent with this one.

>      _timeout: float
>      _app_args: Params | None
> +    _is_privileged: bool = False
<snip>
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 6/6] dts: add statefulness to TestPmdShell
  2024-03-26 19:04 ` [PATCH 6/6] dts: add statefulness to TestPmdShell Luca Vizzarro
@ 2024-03-28 16:48   ` Jeremy Spewock
  2024-04-10  7:41     ` Juraj Linkeš
  2024-04-10  7:50   ` Juraj Linkeš
  1 sibling, 1 reply; 159+ messages in thread
From: Jeremy Spewock @ 2024-03-28 16:48 UTC (permalink / raw)
  To: Luca Vizzarro
  Cc: dev, Juraj Linkeš, Jack Bond-Preston, Honnappa Nagarahalli

On Tue, Mar 26, 2024 at 3:04 PM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> This commit provides a state container for TestPmdShell. It currently
> only indicates whether the packet forwarding has started
> or not, and the number of ports which were given to the shell.
>
> This also fixes the behaviour of `wait_link_status_up` to use the
> command timeout as inherited from InteractiveShell.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Jack Bond-Preston <jack.bond-preston@arm.com>
> Reviewed-by: Honnappa Nagarahalli <honnappa.nagarahalli@arm.com>
> ---
<snip>
> @@ -723,7 +731,13 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
>          if self._app_args.app_params is None:
>              self._app_args.app_params = TestPmdParameters()
>
> -        self.number_of_ports = len(self._app_args.ports) if self._app_args.ports is not None else 0
> +        assert isinstance(self._app_args.app_params, TestPmdParameters)
> +

This is tricky because ideally we wouldn't have the assertion here,
but I understand why it is needed because Eal parameters have app args
which can be any instance of params. I'm not sure of the best way to
solve this, because making testpmd parameters extend from eal would
break the general scheme that you have in place, and having an
extension of EalParameters that enforces this app_args is
TestPmdParameters would solve the issues, but might be a little
clunky. Is there a way we can use a generic to get python to just
understand that, in this case, this will always be TestPmdParameters?
If not I might prefer making a private class where this is
TestPmdParameters, just because there aren't really any other
assertions that we use elsewhere and an unexpected exception from this
(even though I don't think that can happen) could cause people some
issues.

It might be the case that an assertion is the easiest way to deal with
it though, what do you think?

> +        if self._app_args.app_params.auto_start:
> +            self.state.packet_forwarding_started = True
> +
> +        if self._app_args.ports is not None:
> +            self.state.number_of_ports = len(self._app_args.ports)
>
>          super()._start_application(get_privileged_command)
>
<snip>
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 1/6] dts: add parameters data structure
  2024-03-26 19:04 ` [PATCH 1/6] dts: add parameters data structure Luca Vizzarro
  2024-03-28 16:48   ` Jeremy Spewock
@ 2024-04-09 12:10   ` Juraj Linkeš
  2024-04-09 16:28     ` Luca Vizzarro
  1 sibling, 1 reply; 159+ messages in thread
From: Juraj Linkeš @ 2024-04-09 12:10 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Jack Bond-Preston, Honnappa Nagarahalli

On Tue, Mar 26, 2024 at 8:04 PM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> This commit introduces a new "params" module, which adds a new way
> to manage command line parameters. The provided Params dataclass
> is able to read the fields of its child class and produce a string
> representation to supply to the command line. Any data structure
> that is intended to represent command line parameters can inherit
> it. The representation can then manipulated by using the dataclass
> field metadata in conjunction with the provided functions:
>
> * value_only, used to supply a value without forming an option/flag
> * options_end, used to prefix with an options end delimiter (`--`)
> * short, used to define a short parameter name, e.g. `-p`
> * long, used to define a long parameter name, e.g. `--parameter`
> * multiple, used to turn a list into repeating parameters
> * field_mixins, used to manipulate the string representation of the
>   value

We shouldn't list what the patch does, but rather explain the choices
made within. It seems to me the patch is here to give devs an API
instead of them having to construct strings - explaining why this
approach improves the old one should be in the commit message.

>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Jack Bond-Preston <jack.bond-preston@arm.com>
> Reviewed-by: Honnappa Nagarahalli <honnappa.nagarahalli@arm.com>
> ---
>  dts/framework/params.py | 232 ++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 232 insertions(+)
>  create mode 100644 dts/framework/params.py
>
> diff --git a/dts/framework/params.py b/dts/framework/params.py
> new file mode 100644
> index 0000000000..6b48c8353e
> --- /dev/null
> +++ b/dts/framework/params.py
> @@ -0,0 +1,232 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2024 Arm Limited
> +
> +"""Parameter manipulation module.
> +
> +This module provides :class:`~Params` which can be used to model any data structure
> +that is meant to represent any command parameters.
> +"""
> +
> +from dataclasses import dataclass, field, fields
> +from typing import Any, Callable, Literal, Reversible, TypeVar, Iterable
> +from enum import Flag
> +
> +
> +T = TypeVar("T")
> +#: Type for a Mixin.
> +Mixin = Callable[[Any], str]
> +#: Type for an option parameter.
> +Option = Literal[True, None]
> +#: Type for a yes/no option parameter.
> +BooleanOption = Literal[True, False, None]
> +
> +META_VALUE_ONLY = "value_only"
> +META_OPTIONS_END = "options_end"
> +META_SHORT_NAME = "short_name"
> +META_LONG_NAME = "long_name"
> +META_MULTIPLE = "multiple"
> +META_MIXINS = "mixins"

These are only used in the Params class (but not set), so I'd either
move them there or make them private.

> +
> +
> +def value_only(metadata: dict[str, Any] = {}) -> dict[str, Any]:
> +    """Injects the value of the attribute as-is without flag. Metadata modifier for :func:`dataclasses.field`."""
> +    return {**metadata, META_VALUE_ONLY: True}

These methods, on the other hand, are used outside this module, so it
makes sense to have them outside Params. It could be better to have
them inside as static methods, as they're closely related. Looking at
how they're used, we should unite the imports:
1. In testpmd_shell, they're imported directly (from framework.params
import long)
2. In sut_node, just the params module is imported (from framework
import params and then accessed via it: metadata=params.short("l"))

If we move these to Params, we could import Params and use them
similarly to 2. Not sure which one is better.

> +
> +
> +def short(name: str, metadata: dict[str, Any] = {}) -> dict[str, Any]:

It seems to me that having the metadata argument doesn't do anything
in these basic functions.

> +    """Overrides any parameter name with the given short option. Metadata modifier for :func:`dataclasses.field`.
> +
> +    .. code:: python
> +
> +        logical_cores: str | None = field(default="1-4", metadata=short("l"))
> +
> +    will render as ``-l=1-4`` instead of ``--logical-cores=1-4``.
> +    """
> +    return {**metadata, META_SHORT_NAME: name}
> +
> +
> +def long(name: str, metadata: dict[str, Any] = {}) -> dict[str, Any]:
> +    """Overrides the inferred parameter name to the specified one. Metadata modifier for :func:`dataclasses.field`.
> +
> +    .. code:: python
> +
> +        x_name: str | None = field(default="y", metadata=long("x"))
> +
> +    will render as ``--x=y``, but the field is accessed and modified through ``x_name``.
> +    """
> +    return {**metadata, META_LONG_NAME: name}
> +
> +
> +def options_end(metadata: dict[str, Any] = {}) -> dict[str, Any]:
> +    """Precedes the value with an options end delimiter (``--``). Metadata modifier for :func:`dataclasses.field`."""
> +    return {**metadata, META_OPTIONS_END: True}
> +
> +
> +def multiple(metadata: dict[str, Any] = {}) -> dict[str, Any]:
> +    """Specifies that this parameter is set multiple times. Must be a list. Metadata modifier for :func:`dataclasses.field`.
> +
> +    .. code:: python
> +
> +        ports: list[int] | None = field(default_factory=lambda: [0, 1, 2], metadata=multiple(param_name("port")))
> +
> +    will render as ``--port=0 --port=1 --port=2``. Note that modifiers can be chained like in this example.
> +    """
> +    return {**metadata, META_MULTIPLE: True}
> +
> +
> +def field_mixins(*mixins: Mixin, metadata: dict[str, Any] = {}) -> dict[str, Any]:

Any reason why mixins are plural? I've only seen this used with one
argument, do you anticipate we'd need to use more than one? We could
make this singular and if we ever need to do two things, we could just
pass a function that does those two things (or calls two different
functions). Also, I'd just rename the mixin the func or something like
that.

The default of an argument should not be mutable, here's a quick
explanation: https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments

I don't really like the name. The positional arguments are applied to
the value and that should be reflected in the name - I'd like to see
something like convert in the name.

> +    """Takes in a variable number of mixins to manipulate the value's rendering. Metadata modifier for :func:`dataclasses.field`.
> +
> +    The ``metadata`` keyword argument can be used to chain metadata modifiers together.
> +
> +    Mixins can be chained together, executed from right to left in the arguments list order.
> +
> +    Example:
> +
> +    .. code:: python
> +
> +        hex_bitmask: int | None = field(default=0b1101, metadata=field_mixins(hex, metadata=param_name("mask")))
> +
> +    will render as ``--mask=0xd``. The :func:`hex` built-in can be used as a mixin turning a valid integer into a hexadecimal representation.
> +    """
> +    return {**metadata, META_MIXINS: mixins}

metadata | {META_MIXINS: mixins} also creates a new dictionary with
values from both and I think that would be more readable (since it's
mentioned in docs:
https://docs.python.org/3/library/stdtypes.html#mapping-types-dict).

> +
> +
> +def _reduce_mixins(mixins: Reversible[Mixin], value: Any) -> str:
> +    for mixin in reversed(mixins):
> +        value = mixin(value)
> +    return value
> +
> +
> +def str_mixins(*mixins: Mixin):

Again the name, this doesn't strike me as a decorator name. A
decorator modifies the thing it's decorating so it should contain a
verb describing what it's doing.

Maybe we could also do singular mixin here, as I described above. It
may result in cleaner API.

> +    """Decorator which modifies the ``__str__`` method, enabling support for mixins.
> +
> +    Mixins can be chained together, executed from right to left in the arguments list order.
> +
> +    Example:
> +
> +    .. code:: python
> +
> +        @str_mixins(hex_from_flag_value)
> +        class BitMask(enum.Flag):
> +            A = auto()
> +            B = auto()
> +
> +    will allow ``BitMask`` to render as a hexadecimal value.
> +    """
> +
> +    def _class_decorator(original_class):
> +        original_class.__str__ = lambda self: _reduce_mixins(mixins, self)
> +        return original_class
> +
> +    return _class_decorator
> +
> +
> +def comma_separated(values: Iterable[T]) -> str:
> +    """Mixin which renders an iterable in a comma-separated string."""
> +    return ",".join([str(value).strip() for value in values if value is not None])
> +
> +
> +def bracketed(value: str) -> str:
> +    """Mixin which adds round brackets to the input."""
> +    return f"({value})"
> +
> +
> +def str_from_flag_value(flag: Flag) -> str:
> +    """Mixin which returns the value from a :class:`enum.Flag` as a string."""
> +    return str(flag.value)
> +
> +
> +def hex_from_flag_value(flag: Flag) -> str:
> +    """Mixin which turns a :class:`enum.Flag` value into hexadecimal."""
> +    return hex(flag.value)
> +
> +
> +def _make_option(param_name: str, short: bool = False, no: bool = False) -> str:
> +    param_name = param_name.replace("_", "-")
> +    return f"{'-' if short else '--'}{'no-' if no else ''}{param_name}"
> +
> +
> +@dataclass
> +class Params:
> +    """Helper abstract dataclass that renders its fields into command line arguments.

Let's make the abstract class explicitly abstract with
https://docs.python.org/3/library/abc.html#abc.ABC. It won't be a full
abstract class since it won't have any abstract method or properties,
but I think it'll be better this way.

> +
> +    The parameter name is taken from the field name by default. The following:
> +
> +    .. code:: python
> +
> +        name: str | None = "value"
> +
> +    is rendered as ``--name=value``.
> +    Through :func:`dataclasses.field` the resulting parameter can be manipulated by applying
> +    appropriate metadata. This class can be used with the following metadata modifiers:
> +
> +    * :func:`value_only`
> +    * :func:`options_end`
> +    * :func:`short`
> +    * :func:`long`
> +    * :func:`multiple`
> +    * :func:`field_mixins`
> +
> +    To use fields as option switches set the value to ``True`` to enable them. If you

There should be a comma between switches and set.

> +    use a yes/no option switch you can also set ``False`` which would enable an option
> +    prefixed with ``--no-``. Examples:
> +
> +    .. code:: python
> +
> +        interactive: Option = True  # renders --interactive
> +        numa: BooleanOption = False # renders --no-numa

I'm wondering whether these could be simplified, but it's pretty good
this way. I'd change the names though:
Option -> Switch
BooleanOption -> NoSwitch (or YesNoSwitch? NegativeSwitch? Not sure
about this one)

All options (short, long, etc.) are options so that's where it's
confusing. The BooleanOption doesn't really capture what we're doing
with it (prepending no-) so I want a name that captures it.

There could also be some confusion with this being different from the
rest of the options API. This only requires us to set the type, the
rest must be passed in dataclasses.field. It's probably not that big
of a deal though.

> +
> +    Setting ``None`` will disable any option.

I'd reword this to "Setting an option to ``None`` will prevent it from
being rendered." or something similar. Disabling an option is kinda
ambiguous.

The :attr:`~Option` type alias is provided for
> +    regular option switches, whereas :attr:`~BooleanOption` is offered for yes/no ones.
> +
> +    An instance of a dataclass inheriting ``Params`` can also be assigned to an attribute, this helps with grouping parameters
> +    together. The attribute holding the dataclass will be ignored and the latter will just be rendered as expected.
> +    """
> +
> +    def __str__(self) -> str:
> +        arguments: list[str] = []
> +
> +        for field in fields(self):
> +            value = getattr(self, field.name)
> +
> +            if value is None:
> +                continue
> +
> +            options_end = field.metadata.get(META_OPTIONS_END, False)
> +            if options_end:
> +                arguments.append("--")

This is a confusing way to separate the options. The separator itself
is an option (I'd rename it to sep or separator instead of end), so
I'd make a separate field for it. We could pass init=False to field()
in the field definition, but that relies on fields being ordered, so
we'd need to also pass order=True to the dataclass constructor.

> +
> +            value_only = field.metadata.get(META_VALUE_ONLY, False)
> +            if isinstance(value, Params) or value_only or options_end:
> +                arguments.append(str(value))
> +                continue
> +
> +            # take "short_name" metadata, or "long_name" metadata, or infer from field name
> +            option_name = field.metadata.get(
> +                META_SHORT_NAME, field.metadata.get(META_LONG_NAME, field.name)
> +            )
> +            is_short = META_SHORT_NAME in field.metadata
> +
> +            if isinstance(value, bool):
> +                arguments.append(_make_option(option_name, short=is_short, no=(not value)))
> +                continue
> +
> +            option = _make_option(option_name, short=is_short)
> +            separator = " " if is_short else "="
> +            str_mixins = field.metadata.get(META_MIXINS, [])
> +            multiple = field.metadata.get(META_MULTIPLE, False)
> +
> +            values = value if multiple else [value]
> +            for entry_value in values:
> +                entry_value = _reduce_mixins(str_mixins, entry_value)
> +                arguments.append(f"{option}{separator}{entry_value}")
> +
> +        return " ".join(arguments)
> +
> +
> +@dataclass
> +class StrParams(Params):
> +    """A drop-in replacement for parameters passed as a string."""
> +
> +    value: str = field(metadata=value_only())
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 2/6] dts: use Params for interactive shells
  2024-03-28 16:48   ` Jeremy Spewock
@ 2024-04-09 14:56     ` Juraj Linkeš
  2024-04-10  9:34       ` Luca Vizzarro
  0 siblings, 1 reply; 159+ messages in thread
From: Juraj Linkeš @ 2024-04-09 14:56 UTC (permalink / raw)
  To: Jeremy Spewock
  Cc: Luca Vizzarro, dev, Jack Bond-Preston, Honnappa Nagarahalli

On Thu, Mar 28, 2024 at 5:48 PM Jeremy Spewock <jspewock@iol.unh.edu> wrote:
>
> On Tue, Mar 26, 2024 at 3:04 PM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
> >
> > Make it so that interactive shells accept an implementation of `Params`
> > for app arguments. Convert EalParameters to use `Params` instead.
> >
> > String command line parameters can still be supplied by using the
> > `StrParams` implementation.
> >
> > Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> > Reviewed-by: Jack Bond-Preston <jack.bond-preston@arm.com>
> > Reviewed-by: Honnappa Nagarahalli <honnappa.nagarahalli@arm.com>
> > ---
> <snip>
> > @@ -40,7 +42,7 @@ class InteractiveShell(ABC):
> >      _ssh_channel: Channel
> >      _logger: DTSLogger
> >      _timeout: float
> > -    _app_args: str
> > +    _app_args: Params | None
> >
>
> I'm not sure if allowing None should be the solution for these shells
> as opposed to just supplying an empty parameter object. Maybe
> something that could be done is the factory method in sut_node allows
> it to be None, but when it comes time to make the abstract shell it
> creates an empty one if it doesn't exist, or the init method makes
> this an optional parameter but creates it if it doesn't exist.
>
> I suppose this logic would have to exist somewhere because the
> parameters aren't always required, it's just a question of where we
> should put it and if we should just assume that the interactive shell
> class just knows how to accept some parameters and put them into the
> shell. I would maybe leave this as something that cannot be None and
> handle it outside of the shell, but I'm not sure it's something really
> required and I don't have a super strong opinion on it.
>

I think this is an excellent idea. An empty Params (or StrParams or
EalParams if we want to make Params truly abstract and thus not
instantiable) would work as the default value and it would be an
elegant solution since dev will only pass non-empty Params.

> >      #: Prompt to expect at the end of output when sending a command.
> >      #: This is often overridden by subclasses.
> <snip>
> > @@ -118,8 +119,15 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
> >          Also find the number of pci addresses which were allowed on the command line when the app
> >          was started.
> >          """
> > -        self._app_args += " -i --mask-event intr_lsc"
> > -        self.number_of_ports = self._app_args.count("-a ")
> > +        from framework.testbed_model.sut_node import EalParameters
> > +
> > +        assert isinstance(self._app_args, EalParameters)
> > +
> > +        if isinstance(self._app_args.app_params, StrParams):
> > +            self._app_args.app_params.value += " -i --mask-event intr_lsc"
> > +
> > +        self.number_of_ports = len(self._app_args.ports) if self._app_args.ports is not None else 0
>
> I we should override the _app_args parameter in the testpmd shell to
> always be EalParameters instead of doing this import and assertion.
> It's a DPDK app, so we will always need EalParameters anyway, so we
> might as well put that as our typehint to start off as what we expect
> instead.
>
> The checking of an instance of StrParams also feels a little strange
> here, it might be more ideal if we could just add the parameters
> without this check. Maybe something we can do, just because these
> parameters are meant to be CLI commands anyway and will be rendered as
> a string, is replace the StrParams object with a method on the base
> Params dataclass that allows you to just add any string as a value
> only field. Then, we don't have to bother validating anything about
> the app parameters and we don't care what they are, we can just add a
> string to them of new parameters.
>
> I think this is something that likely also gets solved when you
> replace this with testpmd parameters, but it might be a good way to
> make the Params object more versatile in general so that people can
> diverge and add their own flags to it if needed.
>

I'd say this is done because of circular imports. If so, we could move
EalParameters to params.py, that should help. And when we're at it,
either rename it to EalParams or rename the other classes to use the
whole word.

> > +
> >          super()._start_application(get_privileged_command)
> >
> >      def start(self, verify: bool = True) -> None:
>  <snip>
> > @@ -134,7 +136,7 @@ def create_interactive_shell(
> >          shell_cls: Type[InteractiveShellType],
> >          timeout: float,
> >          privileged: bool,
> > -        app_args: str,
> > +        app_args: Params | None,
>
> This also falls in line with what I was saying before about where this
> logic is handled. This was made to always be a string originally
> because by this point it being optional was already handled by the
> sut_node.create_interactive_shell() and we should have some kind of
> value here (even if that value is an empty parameters dataclass) to
> pass into the application. It sort of semantically doesn't really
> change much, but at this point it might as well not be None, so we can
> simplify this type.
>
> >      ) -> InteractiveShellType:
> >          """Factory for interactive session handlers.
> >
> <snip>
> > +@dataclass(kw_only=True)
> > +class EalParameters(Params):
> >      """The environment abstraction layer parameters.
> >
> >      The string representation can be created by converting the instance to a string.
> >      """
> >
> > -    def __init__(
> > -        self,
> > -        lcore_list: LogicalCoreList,
> > -        memory_channels: int,
> > -        prefix: str,
> > -        no_pci: bool,
> > -        vdevs: list[VirtualDevice],
> > -        ports: list[Port],
> > -        other_eal_param: str,
> > -    ):
> > -        """Initialize the parameters according to inputs.
> > -
> > -        Process the parameters into the format used on the command line.
> > +    lcore_list: LogicalCoreList = field(metadata=params.short("l"))
> > +    """The list of logical cores to use."""
> >
> > -        Args:
> > -            lcore_list: The list of logical cores to use.
> > -            memory_channels: The number of memory channels to use.
> > -            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
> > -            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
> > -            vdevs: Virtual devices, e.g.::
> > +    memory_channels: int = field(metadata=params.short("n"))
> > +    """The number of memory channels to use."""
> >
> > -                vdevs=[
> > -                    VirtualDevice('net_ring0'),
> > -                    VirtualDevice('net_ring1')
> > -                ]
> > -            ports: The list of ports to allow.
> > -            other_eal_param: user defined DPDK EAL parameters, e.g.:
> > -                ``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._ports = " ".join(f"-a {port.pci}" for port in ports)
> > -        self._other_eal_param = other_eal_param
> > -
> > -    def __str__(self) -> str:
> > -        """Create the EAL string."""
> > -        return (
> > -            f"{self._lcore_list} "
> > -            f"{self._memory_channels} "
> > -            f"{self._prefix} "
> > -            f"{self._no_pci} "
> > -            f"{self._vdevs} "
> > -            f"{self._ports} "
> > -            f"{self._other_eal_param}"
> > -        )
> > +    prefix: str = field(metadata=params.long("file-prefix"))
> > +    """Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``."""
>
> Just a small note on docstrings, I believe generally in DTS we use the
> google docstring
> (https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
> format where it applies with some small differences. Because these
> attributes aren't class vars however, I believe they should be in the
> docstring for the class in the `Attributes` section. I generally have
> trouble remembering exactly how it should look, but Juraj documented
> it in `doc/guides/tools/dts.rst`
>

I noticed this right away. Here's the bullet point that applies:

* The ``dataclass.dataclass`` decorator changes how the attributes are
processed.
  The dataclass attributes which result in instance variables/attributes
  should also be recorded in the ``Attributes:`` section.

> > +
> > +    no_pci: params.Option
> > +    """Switch to disable PCI bus e.g.: ``no_pci=True``."""
> <snip>
>
> > --
> > 2.34.1
> >

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 1/6] dts: add parameters data structure
  2024-03-28 16:48   ` Jeremy Spewock
@ 2024-04-09 15:52     ` Luca Vizzarro
  0 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-04-09 15:52 UTC (permalink / raw)
  To: Jeremy Spewock
  Cc: dev, Juraj Linkeš, Jack Bond-Preston, Honnappa Nagarahalli

Thank you for your review Jeremy!

On 28/03/2024 16:48, Jeremy Spewock wrote:
> I might add some kind of block comment here as a separator that
> delimits that metadata modifiers start here. Something like what is
> written in scapy.py which creates sections for XML-RPC method vs ones
> that are run by the docker container. This isn't something strictly
> necessary, but it might help break things up and add a little more
> explanation.
<snip>
> You could do the same thing here for mixins, but again, I'm not sure
> it's really necessary.

Yes, I agree that using block comments to delimit sections is a good 
idea. I wasn't sure if we had an established way of doing this, and 
looks like we do!


^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 1/6] dts: add parameters data structure
  2024-04-09 12:10   ` Juraj Linkeš
@ 2024-04-09 16:28     ` Luca Vizzarro
  2024-04-10  9:15       ` Juraj Linkeš
  0 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-04-09 16:28 UTC (permalink / raw)
  To: Juraj Linkeš; +Cc: dev, Jack Bond-Preston, Honnappa Nagarahalli

Thank you so much for your review Juraj!

On 09/04/2024 13:10, Juraj Linkeš wrote:
> We shouldn't list what the patch does, but rather explain the choices
> made within. It seems to me the patch is here to give devs an API
> instead of them having to construct strings - explaining why this
> approach improves the old one should be in the commit message.
>

Ack.
>> +META_VALUE_ONLY = "value_only"
>> +META_OPTIONS_END = "options_end"
>> +META_SHORT_NAME = "short_name"
>> +META_LONG_NAME = "long_name"
>> +META_MULTIPLE = "multiple"
>> +META_MIXINS = "mixins"
> 
> These are only used in the Params class (but not set), so I'd either
> move them there or make them private.
>

Ack.

>> +
>> +def value_only(metadata: dict[str, Any] = {}) -> dict[str, Any]:
>> +    """Injects the value of the attribute as-is without flag. Metadata modifier for :func:`dataclasses.field`."""
>> +    return {**metadata, META_VALUE_ONLY: True}
> 
> These methods, on the other hand, are used outside this module, so it
> makes sense to have them outside Params. It could be better to have
> them inside as static methods, as they're closely related. Looking at
> how they're used, we should unite the imports:
> 1. In testpmd_shell, they're imported directly (from framework.params
> import long)
> 2. In sut_node, just the params module is imported (from framework
> import params and then accessed via it: metadata=params.short("l"))
> 
Having a shorter version may look less verbose. I agree that we can make 
everything a static method of Params, but then having to do Params.short 
etc everytime will make it look more verbose. So what option do we 
prefer? The functions do belong to the params module nonetheless, and 
therefore are meant to be used in conjunction with the Params class.

> If we move these to Params, we could import Params and use them
> similarly to 2. Not sure which one is better.
> 


>> +def field_mixins(*mixins: Mixin, metadata: dict[str, Any] = {}) -> dict[str, Any]:
> 
> Any reason why mixins are plural? I've only seen this used with one
> argument, do you anticipate we'd need to use more than one? We could
> make this singular and if we ever need to do two things, we could just
> pass a function that does those two things (or calls two different
> functions). Also, I'd just rename the mixin the func or something like
> that.
> 
> The default of an argument should not be mutable, here's a quick
> explanation: https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments


Indeed the reason for which I create dictionaries, as I am treating them 
as read only. I wanted to avoid to bloat the code with lots of `is None` 
checks. But we can sacrifice this optimization for better code.

> I don't really like the name. The positional arguments are applied to
> the value and that should be reflected in the name - I'd like to see
> something like convert in the name. 

What this does is effectively just add the mixins to dataclass.field 
metadata, hence "field"_mixins. This is meant to represent a chain of 
mixins, in my original code this appeared more often. Not so much in 
this post, as I made more optimisations. Which takes me to the plural 
bit, for testpmd specifically this plurality appears only when 
decorating another class using `str_mixins`, see TestPmdRingNUMAConfig. 
So for consistency I kept both to ingest a chain of "mixins", as maybe 
it'll be needed in the future.

Are you suggesting to just make the name as singular? But still take 
multiple function pointers? The term "mixin" specifically just means a 
middleware that manipulates the output value when using __str__. Here we 
are just chaining them for reusability. Do you have any better name in mind?

>> +    return {**metadata, META_MIXINS: mixins}
> 
> metadata | {META_MIXINS: mixins} also creates a new dictionary with
> values from both and I think that would be more readable (since it's
> mentioned in docs:
> https://docs.python.org/3/library/stdtypes.html#mapping-types-dict).

If we were to use `None` as default to the arguments, then this would no 
longer be needed. But great shout, wasn't aware of this feature added in 
3.9.

>> +def _reduce_mixins(mixins: Reversible[Mixin], value: Any) -> str:
>> +    for mixin in reversed(mixins):
>> +        value = mixin(value)
>> +    return value
>> +
>> +
>> +def str_mixins(*mixins: Mixin):
> 
> Again the name, this doesn't strike me as a decorator name. A
> decorator modifies the thing it's decorating so it should contain a
> verb describing what it's doing.
> 
> Maybe we could also do singular mixin here, as I described above. It
> may result in cleaner API.

Great point. Yes making the function name sound like a verb is 
definitely the right way. Thank you for pointing it out.

>> +@dataclass
>> +class Params:
>> +    """Helper abstract dataclass that renders its fields into command line arguments.
> 
> Let's make the abstract class explicitly abstract with
> https://docs.python.org/3/library/abc.html#abc.ABC. It won't be a full
> abstract class since it won't have any abstract method or properties,
> but I think it'll be better this way.

No problem. I'm not sure if applying ABC to a dataclass may complicate 
things. But can definitely do.

>> +    To use fields as option switches set the value to ``True`` to enable them. If you
> 
> There should be a comma between switches and set.

Ack.

>> +    use a yes/no option switch you can also set ``False`` which would enable an option
>> +    prefixed with ``--no-``. Examples:
>> +
>> +    .. code:: python
>> +
>> +        interactive: Option = True  # renders --interactive
>> +        numa: BooleanOption = False # renders --no-numa
> 
> I'm wondering whether these could be simplified, but it's pretty good
> this way. I'd change the names though:
> Option -> Switch
> BooleanOption -> NoSwitch (or YesNoSwitch? NegativeSwitch? Not sure
> about this one)
> 
> All options (short, long, etc.) are options so that's where it's
> confusing. The BooleanOption doesn't really capture what we're doing
> with it (prepending no-) so I want a name that captures it.
> 
> There could also be some confusion with this being different from the
> rest of the options API. This only requires us to set the type, the
> rest must be passed in dataclasses.field. It's probably not that big
> of a deal though.
> 

I am glad you are bringing this up. I am also perplexed on the choice of 
name here. I decided to use whatever libc getopts uses. But your 
suggestion sounds nice. Will use it.

>> +
>> +    Setting ``None`` will disable any option.
> 
> I'd reword this to "Setting an option to ``None`` will prevent it from
> being rendered." or something similar. Disabling an option is kinda
> ambiguous.
> 

Ack.

>> +            options_end = field.metadata.get(META_OPTIONS_END, False)
>> +            if options_end:
>> +                arguments.append("--")
> 
> This is a confusing way to separate the options. The separator itself
> is an option (I'd rename it to sep or separator instead of end), so
> I'd make a separate field for it. We could pass init=False to field()
> in the field definition, but that relies on fields being ordered, so
> we'd need to also pass order=True to the dataclass constructor.

Ack.

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 3/6] dts: add testpmd shell params
  2024-03-26 19:04 ` [PATCH 3/6] dts: add testpmd shell params Luca Vizzarro
  2024-03-28 16:48   ` Jeremy Spewock
@ 2024-04-09 16:37   ` Juraj Linkeš
  2024-04-10 10:49     ` Luca Vizzarro
  1 sibling, 1 reply; 159+ messages in thread
From: Juraj Linkeš @ 2024-04-09 16:37 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Jack Bond-Preston, Honnappa Nagarahalli

As Jeremy pointed out, going forward, this is likely to become bloated
and moving it to params.py (for example) may be better.

There's a lot of testpmd args here. I commented on the implementation
of some of them. I didn't verify that the actual values match the docs
or, god forbid, tested all of it. :-) Doing that as we start using
them is going to be good enough.

On Tue, Mar 26, 2024 at 8:04 PM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Implement all the testpmd shell parameters into a data structure.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Jack Bond-Preston <jack.bond-preston@arm.com>
> Reviewed-by: Honnappa Nagarahalli <honnappa.nagarahalli@arm.com>
> ---
>  dts/framework/remote_session/testpmd_shell.py | 633 +++++++++++++++++-
>  1 file changed, 615 insertions(+), 18 deletions(-)
>
> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
> index db3abb7600..a823dc53be 100644
> --- a/dts/framework/remote_session/testpmd_shell.py
> +++ b/dts/framework/remote_session/testpmd_shell.py

<snip>

> +@str_mixins(bracketed, comma_separated)
> +class TestPmdRingNUMAConfig(NamedTuple):
> +    """Tuple associating DPDK port, direction of the flow and NUMA socket."""

Is there any particular order for these various classes?

> +
> +    port: int
> +    direction: TestPmdFlowDirection
> +    socket: int
> +
> +

<snip>

> +@dataclass(kw_only=True)
> +class TestPmdTXOnlyForwardingMode(Params):

The three special forwarding modes should really be moved right after
TestPmdForwardingModes. Do we actually need these three in
TestPmdForwardingModes? Looks like we could just remove those from
TestPmdForwardingModes since they have to be passed separately, not as
that Enum.

> +    __forward_mode: Literal[TestPmdForwardingModes.txonly] = field(
> +        default=TestPmdForwardingModes.txonly, init=False, metadata=long("forward-mode")
> +    )

I guess this is here so that "--forward-mode=txonly" gets rendered,
right? Why the two underscored? Is that because we want to hammer home
the fact that this is init=False, a kind of internal field? I'd like
to make it like the other fields, without any underscores (or maybe
just one underscore), and documented (definitely documented).
If we remove txonly from the Enum, we could just have the string value
here. The Enums are mostly useful to give users the proper range of
values.

> +    multi_flow: Option = field(default=None, metadata=long("txonly-multi-flow"))
> +    """Generate multiple flows."""
> +    segments_length: XYPair | None = field(default=None, metadata=long("txpkts"))
> +    """Set TX segment sizes or total packet length."""
> +
> +
> +@dataclass(kw_only=True)
> +class TestPmdFlowGenForwardingMode(Params):
> +    __forward_mode: Literal[TestPmdForwardingModes.flowgen] = field(
> +        default=TestPmdForwardingModes.flowgen, init=False, metadata=long("forward-mode")
> +    )
> +    clones: int | None = field(default=None, metadata=long("flowgen-clones"))
> +    """Set the number of each packet clones to be sent. Sending clones reduces host CPU load on
> +    creating packets and may help in testing extreme speeds or maxing out Tx packet performance.
> +    N should be not zero, but less than ‘burst’ parameter.
> +    """
> +    flows: int | None = field(default=None, metadata=long("flowgen-flows"))
> +    """Set the number of flows to be generated, where 1 <= N <= INT32_MAX."""
> +    segments_length: XYPair | None = field(default=None, metadata=long("txpkts"))
> +    """Set TX segment sizes or total packet length."""
> +
> +
> +@dataclass(kw_only=True)
> +class TestPmdNoisyForwardingMode(Params):
> +    __forward_mode: Literal[TestPmdForwardingModes.noisy] = field(
> +        default=TestPmdForwardingModes.noisy, init=False, metadata=long("forward-mode")
> +    )

Are both of __forward_mode and forward_mode needed because we need to
render both?

> +    forward_mode: (
> +        Literal[
> +            TestPmdForwardingModes.io,
> +            TestPmdForwardingModes.mac,
> +            TestPmdForwardingModes.macswap,
> +            TestPmdForwardingModes.fivetswap,
> +        ]
> +        | None

Is there a difference between using union (TestPmdForwardingModes.io |
TestPmdForwardingModes.mac etc.) and Literal?

> +    ) = field(default=TestPmdForwardingModes.io, metadata=long("noisy-forward-mode"))
> +    """Set the noisy vnf forwarding mode."""
> +    tx_sw_buffer_size: int | None = field(default=None, metadata=long("noisy-tx-sw-buffer-size"))
> +    """Set the maximum number of elements of the FIFO queue to be created for buffering packets.
> +    The default value is 0.
> +    """
> +    tx_sw_buffer_flushtime: int | None = field(
> +        default=None, metadata=long("noisy-tx-sw-buffer-flushtime")
> +    )
> +    """Set the time before packets in the FIFO queue are flushed. The default value is 0."""
> +    lkup_memory: int | None = field(default=None, metadata=long("noisy-lkup-memory"))
> +    """Set the size of the noisy neighbor simulation memory buffer in MB to N. The default value is 0."""
> +    lkup_num_reads: int | None = field(default=None, metadata=long("noisy-lkup-num-reads"))
> +    """Set the number of reads to be done in noisy neighbor simulation memory buffer to N.
> +    The default value is 0.
> +    """
> +    lkup_num_writes: int | None = field(default=None, metadata=long("noisy-lkup-num-writes"))
> +    """Set the number of writes to be done in noisy neighbor simulation memory buffer to N.
> +    The default value is 0.
> +    """
> +    lkup_num_reads_writes: int | None = field(
> +        default=None, metadata=long("noisy-lkup-num-reads-writes")
> +    )
> +    """Set the number of r/w accesses to be done in noisy neighbor simulation memory buffer to N.
> +    The default value is 0.
> +    """

<snip>

> +@dataclass
> +class TestPmdDisableRSS(Params):
> +    """Disable RSS (Receive Side Scaling)."""

Let's put the explanation/reminder of what RSS stands for to either
all three classes or none of them.

> +
> +    __disable_rss: Literal[True] = field(default=True, init=False, metadata=long("disable-rss"))
> +
> +
> +@dataclass
> +class TestPmdSetRSSIPOnly(Params):
> +    """Set RSS functions for IPv4/IPv6 only."""
> +
> +    __rss_ip: Literal[True] = field(default=True, init=False, metadata=long("rss-ip"))
> +
> +
> +@dataclass
> +class TestPmdSetRSSUDP(Params):
> +    """Set RSS functions for IPv4/IPv6 and UDP."""
> +
> +    __rss_udp: Literal[True] = field(default=True, init=False, metadata=long("rss-udp"))
> +
> +

<snip>

> +    rss: TestPmdDisableRSS | TestPmdSetRSSIPOnly | TestPmdSetRSSUDP | None = None
> +    """RSS option setting.
> +
> +    The value can be one of:
> +    * :class:`TestPmdDisableRSS`, to disable RSS
> +    * :class:`TestPmdSetRSSIPOnly`, to set RSS for IPv4/IPv6 only
> +    * :class:`TestPmdSetRSSUDP`, to set RSS for IPv4/IPv6 and UDP
> +    """

Have you thought about making an Enum where values would be these
classes? That could simplify things a bit for users if it works.

> +
> +    forward_mode: (
> +        Literal[
> +            TestPmdForwardingModes.io,
> +            TestPmdForwardingModes.mac,
> +            TestPmdForwardingModes.macswap,
> +            TestPmdForwardingModes.rxonly,
> +            TestPmdForwardingModes.csum,
> +            TestPmdForwardingModes.icmpecho,
> +            TestPmdForwardingModes.ieee1588,
> +            TestPmdForwardingModes.fivetswap,
> +            TestPmdForwardingModes.shared_rxq,
> +            TestPmdForwardingModes.recycle_mbufs,
> +        ]

This could result in just TestPmdForwardingModes | the rest if we
remove the compound fw modes from TestPmdForwardingModes. Maybe we
could rename TestPmdForwardingModes to TestPmdSimpleForwardingModes or
something at that point.

> +        | TestPmdFlowGenForwardingMode
> +        | TestPmdTXOnlyForwardingMode
> +        | TestPmdNoisyForwardingMode
> +        | None
> +    ) = TestPmdForwardingModes.io
> +    """Set the forwarding mode.

<snip>

> +    mempool_allocation_mode: (
> +        Literal[
> +            TestPmdMempoolAllocationMode.native,
> +            TestPmdMempoolAllocationMode.xmem,
> +            TestPmdMempoolAllocationMode.xmemhuge,
> +        ]
> +        | TestPmdAnonMempoolAllocationMode
> +        | None

This looks similar to fw modes, maybe the same applies here as well.

> +    ) = field(default=None, metadata=long("mp-alloc"))
> +    """Select mempool allocation mode.
> +
> +    The value can be one of:
> +    * :attr:`TestPmdMempoolAllocationMode.native`
> +    * :class:`TestPmdAnonMempoolAllocationMode`
> +    * :attr:`TestPmdMempoolAllocationMode.xmem`
> +    * :attr:`TestPmdMempoolAllocationMode.xmemhuge`
> +    """

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 4/6] dts: use testpmd params for scatter test suite
  2024-03-26 19:04 ` [PATCH 4/6] dts: use testpmd params for scatter test suite Luca Vizzarro
@ 2024-04-09 19:12   ` Juraj Linkeš
  2024-04-10 10:53     ` Luca Vizzarro
  0 siblings, 1 reply; 159+ messages in thread
From: Juraj Linkeš @ 2024-04-09 19:12 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Jack Bond-Preston, Honnappa Nagarahalli

On Tue, Mar 26, 2024 at 8:04 PM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Update the buffer scatter test suite to use TestPmdParameters
> instead of the StrParams implementation.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Jack Bond-Preston <jack.bond-preston@arm.com>
> Reviewed-by: Honnappa Nagarahalli <honnappa.nagarahalli@arm.com>
> ---
>  dts/tests/TestSuite_pmd_buffer_scatter.py | 19 +++++++++++--------
>  1 file changed, 11 insertions(+), 8 deletions(-)
>
> diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
> index 4cdbdc4272..c6d313fc64 100644
> --- a/dts/tests/TestSuite_pmd_buffer_scatter.py
> +++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
> @@ -23,7 +23,11 @@
>  from scapy.utils import hexstr  # type: ignore[import]
>
>  from framework.params import StrParams
> -from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
> +from framework.remote_session.testpmd_shell import (
> +    TestPmdForwardingModes,
> +    TestPmdShell,
> +    TestPmdParameters,
> +)
>  from framework.test_suite import TestSuite
>
>
> @@ -104,16 +108,15 @@ def pmd_scatter(self, mbsize: int) -> None:
>          """
>          testpmd = self.sut_node.create_interactive_shell(
>              TestPmdShell,
> -            app_parameters=StrParams(
> -                "--mbcache=200 "
> -                f"--mbuf-size={mbsize} "
> -                "--max-pkt-len=9000 "
> -                "--port-topology=paired "
> -                "--tx-offloads=0x00008000"
> +            app_parameters=TestPmdParameters(
> +                forward_mode=TestPmdForwardingModes.mac,
> +                mbcache=200,
> +                mbuf_size=[mbsize],
> +                max_pkt_len=9000,
> +                tx_offloads=0x00008000,
>              ),
>              privileged=True,
>          )
> -        testpmd.set_forward_mode(TestPmdForwardingModes.mac)

Jeremy, does this change the test? Instead of configuring the fw mode
after starting testpmd, we're starting testpmd with fw mode
configured.

If not, we should remove the testpmd.set_forward_mode method, as it's
not used anymore.

>          testpmd.start()
>
>          for offset in [-1, 0, 1, 4, 5]:
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 5/6] dts: add statefulness to InteractiveShell
  2024-03-28 16:48   ` Jeremy Spewock
@ 2024-04-10  6:53     ` Juraj Linkeš
  2024-04-10 11:27       ` Luca Vizzarro
  0 siblings, 1 reply; 159+ messages in thread
From: Juraj Linkeš @ 2024-04-10  6:53 UTC (permalink / raw)
  To: Jeremy Spewock
  Cc: Luca Vizzarro, dev, Jack Bond-Preston, Honnappa Nagarahalli

I have a general question. What are these changes for? Do you
anticipate us needing this in the future? Wouldn't it be better to add
it only when we need it?

On Thu, Mar 28, 2024 at 5:48 PM Jeremy Spewock <jspewock@iol.unh.edu> wrote:
>
> On Tue, Mar 26, 2024 at 3:04 PM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
> <snip>
> > diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
> > index a2c7b30d9f..5d80061e8d 100644
> > --- a/dts/framework/remote_session/interactive_shell.py
> > +++ b/dts/framework/remote_session/interactive_shell.py
> > @@ -41,8 +41,10 @@ class InteractiveShell(ABC):
> >      _stdout: channel.ChannelFile
> >      _ssh_channel: Channel
> >      _logger: DTSLogger
> > +    __default_timeout: float
>
> Only single underscores are used for other private variables, probably
> better to keep that consistent with this one.
>

I agree, I don't see a reason for the double underscore.

> >      _timeout: float
> >      _app_args: Params | None
> > +    _is_privileged: bool = False
> <snip>
> > 2.34.1
> >

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 6/6] dts: add statefulness to TestPmdShell
  2024-03-28 16:48   ` Jeremy Spewock
@ 2024-04-10  7:41     ` Juraj Linkeš
  2024-04-10 11:35       ` Luca Vizzarro
  0 siblings, 1 reply; 159+ messages in thread
From: Juraj Linkeš @ 2024-04-10  7:41 UTC (permalink / raw)
  To: Jeremy Spewock
  Cc: Luca Vizzarro, dev, Jack Bond-Preston, Honnappa Nagarahalli

On Thu, Mar 28, 2024 at 5:49 PM Jeremy Spewock <jspewock@iol.unh.edu> wrote:
>
> On Tue, Mar 26, 2024 at 3:04 PM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
> >
> > This commit provides a state container for TestPmdShell. It currently
> > only indicates whether the packet forwarding has started
> > or not, and the number of ports which were given to the shell.
> >
> > This also fixes the behaviour of `wait_link_status_up` to use the
> > command timeout as inherited from InteractiveShell.
> >
> > Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> > Reviewed-by: Jack Bond-Preston <jack.bond-preston@arm.com>
> > Reviewed-by: Honnappa Nagarahalli <honnappa.nagarahalli@arm.com>
> > ---
> <snip>
> > @@ -723,7 +731,13 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
> >          if self._app_args.app_params is None:
> >              self._app_args.app_params = TestPmdParameters()
> >
> > -        self.number_of_ports = len(self._app_args.ports) if self._app_args.ports is not None else 0
> > +        assert isinstance(self._app_args.app_params, TestPmdParameters)
> > +
>
> This is tricky because ideally we wouldn't have the assertion here,
> but I understand why it is needed because Eal parameters have app args
> which can be any instance of params. I'm not sure of the best way to
> solve this, because making testpmd parameters extend from eal would
> break the general scheme that you have in place, and having an
> extension of EalParameters that enforces this app_args is
> TestPmdParameters would solve the issues, but might be a little
> clunky. Is there a way we can use a generic to get python to just
> understand that, in this case, this will always be TestPmdParameters?
> If not I might prefer making a private class where this is
> TestPmdParameters, just because there aren't really any other
> assertions that we use elsewhere and an unexpected exception from this
> (even though I don't think that can happen) could cause people some
> issues.
>
> It might be the case that an assertion is the easiest way to deal with
> it though, what do you think?
>

We could change the signature (just the type of app_args) of the init
method - I think we should be able to create a type that's
EalParameters with .app_params being TestPmdParameters or None. The
init method would just call super().

Something like the above is basically necessary with inheritance where
subclasses are all extensions (not just implementations) of the
superclass (having differences in API).

> > +        if self._app_args.app_params.auto_start:
> > +            self.state.packet_forwarding_started = True
> > +
> > +        if self._app_args.ports is not None:
> > +            self.state.number_of_ports = len(self._app_args.ports)
> >
> >          super()._start_application(get_privileged_command)
> >
> <snip>
> > 2.34.1
> >

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 6/6] dts: add statefulness to TestPmdShell
  2024-03-26 19:04 ` [PATCH 6/6] dts: add statefulness to TestPmdShell Luca Vizzarro
  2024-03-28 16:48   ` Jeremy Spewock
@ 2024-04-10  7:50   ` Juraj Linkeš
  2024-04-10 11:37     ` Luca Vizzarro
  1 sibling, 1 reply; 159+ messages in thread
From: Juraj Linkeš @ 2024-04-10  7:50 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Jack Bond-Preston, Honnappa Nagarahalli

On Tue, Mar 26, 2024 at 8:04 PM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> This commit provides a state container for TestPmdShell. It currently
> only indicates whether the packet forwarding has started
> or not, and the number of ports which were given to the shell.
>

A reminder, the commit message should explain why we're doing this
change, not what the change is.

> This also fixes the behaviour of `wait_link_status_up` to use the
> command timeout as inherited from InteractiveShell.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Jack Bond-Preston <jack.bond-preston@arm.com>
> Reviewed-by: Honnappa Nagarahalli <honnappa.nagarahalli@arm.com>
> ---
>  dts/framework/remote_session/testpmd_shell.py | 41 +++++++++++++------
>  1 file changed, 28 insertions(+), 13 deletions(-)
>
> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
> index a823dc53be..ea1d254f86 100644
> --- a/dts/framework/remote_session/testpmd_shell.py
> +++ b/dts/framework/remote_session/testpmd_shell.py
> @@ -678,19 +678,27 @@ def __str__(self) -> str:
>          return self.pci_address
>
>
> +@dataclass(slots=True)
> +class TestPmdState:
> +    """Session state container."""
> +
> +    #:
> +    packet_forwarding_started: bool = False

The same question as in the previous patch, do you anticipate this
being needed and should we add this only when it's actually used?

> +
> +    #: The number of ports which were allowed on the command-line when testpmd was started.
> +    number_of_ports: int = 0
> +
> +
>  class TestPmdShell(InteractiveShell):
>      """Testpmd interactive shell.
>
>      The testpmd shell users should never use
>      the :meth:`~.interactive_shell.InteractiveShell.send_command` method directly, but rather
>      call specialized methods. If there isn't one that satisfies a need, it should be added.
> -
> -    Attributes:
> -        number_of_ports: The number of ports which were allowed on the command-line when testpmd
> -            was started.
>      """
>
> -    number_of_ports: int
> +    #: Current state
> +    state: TestPmdState = TestPmdState()

Assigning a value makes this a class variable, shared across all
instances. This should be initialized in __init__().

But do we actually want to do this via composition? We'd need to
access the attributes via .state all the time and I don't really like
that. We could just put them into TestPmdShell directly, initializing
them in __init__().

>
>      #: The path to the testpmd executable.
>      path: ClassVar[PurePath] = PurePath("app", "dpdk-testpmd")

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 1/6] dts: add parameters data structure
  2024-04-09 16:28     ` Luca Vizzarro
@ 2024-04-10  9:15       ` Juraj Linkeš
  2024-04-10  9:51         ` Luca Vizzarro
  0 siblings, 1 reply; 159+ messages in thread
From: Juraj Linkeš @ 2024-04-10  9:15 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Jack Bond-Preston, Honnappa Nagarahalli

On Tue, Apr 9, 2024 at 6:28 PM Luca Vizzarro <Luca.Vizzarro@arm.com> wrote:
>
> Thank you so much for your review Juraj!
>

You're welcome!

> >> +
> >> +def value_only(metadata: dict[str, Any] = {}) -> dict[str, Any]:
> >> +    """Injects the value of the attribute as-is without flag. Metadata modifier for :func:`dataclasses.field`."""
> >> +    return {**metadata, META_VALUE_ONLY: True}
> >
> > These methods, on the other hand, are used outside this module, so it
> > makes sense to have them outside Params. It could be better to have
> > them inside as static methods, as they're closely related. Looking at
> > how they're used, we should unite the imports:
> > 1. In testpmd_shell, they're imported directly (from framework.params
> > import long)
> > 2. In sut_node, just the params module is imported (from framework
> > import params and then accessed via it: metadata=params.short("l"))
> >
> Having a shorter version may look less verbose. I agree that we can make
> everything a static method of Params, but then having to do Params.short
> etc everytime will make it look more verbose. So what option do we
> prefer? The functions do belong to the params module nonetheless, and
> therefore are meant to be used in conjunction with the Params class.
>

When I first saw the code, I liked the usage in sut_node better, e.g.:
prefix: str = field(metadata=params.long("file-prefix")). I think this
is because it's obvious where the function comes from. I'd do the
longer version because I think most people are just going to glance at
the code when they want to know how to properly implement an argument
so the explicit nature could help with understanding how it should be
done.

> > If we move these to Params, we could import Params and use them
> > similarly to 2. Not sure which one is better.
> >
>
>
> >> +def field_mixins(*mixins: Mixin, metadata: dict[str, Any] = {}) -> dict[str, Any]:
> >
> > Any reason why mixins are plural? I've only seen this used with one
> > argument, do you anticipate we'd need to use more than one? We could
> > make this singular and if we ever need to do two things, we could just
> > pass a function that does those two things (or calls two different
> > functions). Also, I'd just rename the mixin the func or something like
> > that.
> >
> > The default of an argument should not be mutable, here's a quick
> > explanation: https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments
>
>
> Indeed the reason for which I create dictionaries, as I am treating them
> as read only. I wanted to avoid to bloat the code with lots of `is None`
> checks. But we can sacrifice this optimization for better code.
>

This would be the only place where we'd do the check, as I don't think
we need the metadata argument in any of the other functions - those
seem to be mutually exclusive, but maybe they're not? In any case,
we'd need to fix this, I don't think treating them as read-only avoids
the problem.

> > I don't really like the name. The positional arguments are applied to
> > the value and that should be reflected in the name - I'd like to see
> > something like convert in the name.
>
> What this does is effectively just add the mixins to dataclass.field
> metadata, hence "field"_mixins. This is meant to represent a chain of
> mixins, in my original code this appeared more often. Not so much in
> this post, as I made more optimisations. Which takes me to the plural
> bit, for testpmd specifically this plurality appears only when
> decorating another class using `str_mixins`, see TestPmdRingNUMAConfig.
> So for consistency I kept both to ingest a chain of "mixins", as maybe
> it'll be needed in the future.
>
> Are you suggesting to just make the name as singular? But still take
> multiple function pointers?

Singular with one function, as that was what I saw being used. The one
function could do multiple things (or call multiple other functions)
if a need arises. The str_mixins could be used this way as well.

I don't know which one is better, maybe keeping the plural is fine.

> The term "mixin" specifically just means a
> middleware that manipulates the output value when using __str__.

Aren't all of the other functions mixins as well, at least in some
sense? They change the option, not the value, but could still be
thought of as mixins in some respect.

> Here we
> are just chaining them for reusability. Do you have any better name in mind?
>

I don't know, so let's brainstorm a bit. Let's start with the usage:
portmask: int | None = field(default=None, metadata=field_mixins(hex))

Here it's not clear at all why it's called field_mixins, at least
compared to the other functions which are not called mixins. I guess
the other functions are predefined option mixins whereas we're
supplying our own value mixins here. I also noticed that there's a bit
of an inconsistency with the naming. The basic functions (long etc.)
don't have the "field_" prefix, but this one does. Maybe a better name
would be custom_mixins? Or value_mixins? Or custom_value? Or maybe
convert_value? I like the last one:
portmask: int | None = field(default=None, metadata=convert_value(hex))
metadata=params.convert_value(_port_to_pci,
metadata=params.multiple(params.short("a"))), # in sut_node

I think this is easier to grasp. I'm thinking about whether we need to
have mixin(s) in the name and I don't think it adds much. If I'm a
developer, I'm looking at these functions and I stumble upon
convert_value, what I'm thinking is "Nice, I can do some conversion on
the values I pass, how do I do that?", then I look at the signature
and find out that I expect, that is I need to pass a function (or
multiple function if I want to). I guess this comes down to the
function name (field_mixins) not conveying what it's doing, rather
what you're passing to it.

So my conclusion from this brainstorming is that a better name would
be convert_value. :-)

Also, unrelated, but the context is lost. Another thing I just noticed
is in the docstring:
The ``metadata`` keyword argument can be used to chain metadata
modifiers together.

We're missing the Args: section in all of the docstrings (where we
could put the above). Also the Returns: section.

> >> +    return {**metadata, META_MIXINS: mixins}
> >
> > metadata | {META_MIXINS: mixins} also creates a new dictionary with
> > values from both and I think that would be more readable (since it's
> > mentioned in docs:
> > https://docs.python.org/3/library/stdtypes.html#mapping-types-dict).
>
> If we were to use `None` as default to the arguments, then this would no
> longer be needed. But great shout, wasn't aware of this feature added in
> 3.9.
>

It wouldn't? We'd still have to merge the dicts when metadata is not None, no?

<snip>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 2/6] dts: use Params for interactive shells
  2024-04-09 14:56     ` Juraj Linkeš
@ 2024-04-10  9:34       ` Luca Vizzarro
  2024-04-10  9:58         ` Juraj Linkeš
  0 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-04-10  9:34 UTC (permalink / raw)
  To: Juraj Linkeš, Jeremy Spewock
  Cc: dev, Jack Bond-Preston, Honnappa Nagarahalli

On 09/04/2024 15:56, Juraj Linkeš wrote:
> On Thu, Mar 28, 2024 at 5:48 PM Jeremy Spewock <jspewock@iol.unh.edu> wrote:
>>
>> I'm not sure if allowing None should be the solution for these shells
>> as opposed to just supplying an empty parameter object. Maybe
>> something that could be done is the factory method in sut_node allows
>> it to be None, but when it comes time to make the abstract shell it
>> creates an empty one if it doesn't exist, or the init method makes
>> this an optional parameter but creates it if it doesn't exist.
>>
>> I suppose this logic would have to exist somewhere because the
>> parameters aren't always required, it's just a question of where we
>> should put it and if we should just assume that the interactive shell
>> class just knows how to accept some parameters and put them into the
>> shell. I would maybe leave this as something that cannot be None and
>> handle it outside of the shell, but I'm not sure it's something really
>> required and I don't have a super strong opinion on it.
>>
> 
> I think this is an excellent idea. An empty Params (or StrParams or
> EalParams if we want to make Params truly abstract and thus not
> instantiable) would work as the default value and it would be an
> elegant solution since dev will only pass non-empty Params.
> 

I left it as generic as it could get as I honestly couldn't grasp the 
full picture. I am really keen to ditch everything else and use an empty 
Params object instead for defaults. And as Juraj said, if I am making 
Params a true abstract object, then it'd be picking one of the Params 
subclasses mentioned above.

I guess EalParams could only be used with shells are that sure to be 
DPDK apps, and I presume that's only TestPmdShell for now.

>>> +        from framework.testbed_model.sut_node import EalParameters
>>> +
>>> +        assert isinstance(self._app_args, EalParameters)
>>> +
>>> +        if isinstance(self._app_args.app_params, StrParams):
>>> +            self._app_args.app_params.value += " -i --mask-event intr_lsc"
>>> +
>>> +        self.number_of_ports = len(self._app_args.ports) if self._app_args.ports is not None else 0
>>
>> I we should override the _app_args parameter in the testpmd shell to
>> always be EalParameters instead of doing this import and assertion.
>> It's a DPDK app, so we will always need EalParameters anyway, so we
>> might as well put that as our typehint to start off as what we expect
>> instead.
>>
>> The checking of an instance of StrParams also feels a little strange
>> here, it might be more ideal if we could just add the parameters
>> without this check. Maybe something we can do, just because these
>> parameters are meant to be CLI commands anyway and will be rendered as
>> a string, is replace the StrParams object with a method on the base
>> Params dataclass that allows you to just add any string as a value
>> only field. Then, we don't have to bother validating anything about
>> the app parameters and we don't care what they are, we can just add a
>> string to them of new parameters.
>>
>> I think this is something that likely also gets solved when you
>> replace this with testpmd parameters, but it might be a good way to
>> make the Params object more versatile in general so that people can
>> diverge and add their own flags to it if needed.

Jeremy, I agree this is not ideal. Although this was meant only to be 
transitionary for the next commit (as you say it gets resolved with 
TestPmdParams). But I agree with you that we can integrate the StrParams 
facility within Params. This means no longer making Params a true 
abstract class though, which is something I can live with, especially if 
we can make it simpler.

> I'd say this is done because of circular imports. If so, we could move
> EalParameters to params.py, that should help. And when we're at it,
> either rename it to EalParams or rename the other classes to use the
> whole word.

Yeah, the circular imports are the main problem indeed. I considered 
refactoring but noticed it'd take more than just moving EalParam(eter)s 
to params.py. Nonetheless, keen to make a bigger refactoring to make 
this happen.
>>> +
>>>           super()._start_application(get_privileged_command)
>>>
>>>       def start(self, verify: bool = True) -> None:
>>   <snip>
>>> @@ -134,7 +136,7 @@ def create_interactive_shell(
>>>           shell_cls: Type[InteractiveShellType],
>>>           timeout: float,
>>>           privileged: bool,
>>> -        app_args: str,
>>> +        app_args: Params | None,
>>
>> This also falls in line with what I was saying before about where this
>> logic is handled. This was made to always be a string originally
>> because by this point it being optional was already handled by the
>> sut_node.create_interactive_shell() and we should have some kind of
>> value here (even if that value is an empty parameters dataclass) to
>> pass into the application. It sort of semantically doesn't really
>> change much, but at this point it might as well not be None, so we can
>> simplify this type.
Ack.
>>>       ) -> InteractiveShellType:
>>>           """Factory for interactive session handlers.
>>>
<snip>
>>> +@dataclass(kw_only=True)
>>> +class EalParameters(Params):
>>>       """The environment abstraction layer parameters.
>>>
>>>       The string representation can be created by converting the instance to a string.
>>>       """
>>>
>>> -    def __init__(
>>> -        self,
>>> -        lcore_list: LogicalCoreList,
>>> -        memory_channels: int,
>>> -        prefix: str,
>>> -        no_pci: bool,
>>> -        vdevs: list[VirtualDevice],
>>> -        ports: list[Port],
>>> -        other_eal_param: str,
>>> -    ):
>>> -        """Initialize the parameters according to inputs.
>>> -
>>> -        Process the parameters into the format used on the command line.
>>> +    lcore_list: LogicalCoreList = field(metadata=params.short("l"))
>>> +    """The list of logical cores to use."""
>>>
>>> -        Args:
>>> -            lcore_list: The list of logical cores to use.
>>> -            memory_channels: The number of memory channels to use.
>>> -            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
>>> -            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
>>> -            vdevs: Virtual devices, e.g.::
>>> +    memory_channels: int = field(metadata=params.short("n"))
>>> +    """The number of memory channels to use."""
>>>
>>> -                vdevs=[
>>> -                    VirtualDevice('net_ring0'),
>>> -                    VirtualDevice('net_ring1')
>>> -                ]
>>> -            ports: The list of ports to allow.
>>> -            other_eal_param: user defined DPDK EAL parameters, e.g.:
>>> -                ``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._ports = " ".join(f"-a {port.pci}" for port in ports)
>>> -        self._other_eal_param = other_eal_param
>>> -
>>> -    def __str__(self) -> str:
>>> -        """Create the EAL string."""
>>> -        return (
>>> -            f"{self._lcore_list} "
>>> -            f"{self._memory_channels} "
>>> -            f"{self._prefix} "
>>> -            f"{self._no_pci} "
>>> -            f"{self._vdevs} "
>>> -            f"{self._ports} "
>>> -            f"{self._other_eal_param}"
>>> -        )
>>> +    prefix: str = field(metadata=params.long("file-prefix"))
>>> +    """Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``."""
>>
>> Just a small note on docstrings, I believe generally in DTS we use the
>> google docstring
>> (https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
>> format where it applies with some small differences. Because these
>> attributes aren't class vars however, I believe they should be in the
>> docstring for the class in the `Attributes` section. I generally have
>> trouble remembering exactly how it should look, but Juraj documented
>> it in `doc/guides/tools/dts.rst`
>>
> 
> I noticed this right away. Here's the bullet point that applies:
> 
> * The ``dataclass.dataclass`` decorator changes how the attributes are
> processed.
>    The dataclass attributes which result in instance variables/attributes
>    should also be recorded in the ``Attributes:`` section.
>

I truly did not even for a second recognise the distinction. But this 
explains a lot. So the idea here is to move every documented field as an 
attribute in the main class docstring?

>>> +
>>> +    no_pci: params.Option
>>> +    """Switch to disable PCI bus e.g.: ``no_pci=True``."""
>> <snip>
>>
>>> --
>>> 2.34.1
>>>


^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 1/6] dts: add parameters data structure
  2024-04-10  9:15       ` Juraj Linkeš
@ 2024-04-10  9:51         ` Luca Vizzarro
  2024-04-10 10:04           ` Juraj Linkeš
  0 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-04-10  9:51 UTC (permalink / raw)
  To: Juraj Linkeš; +Cc: dev, Jack Bond-Preston, Honnappa Nagarahalli

On 10/04/2024 10:15, Juraj Linkeš wrote:
>>>> +
>>>> +def value_only(metadata: dict[str, Any] = {}) -> dict[str, Any]:
>>>> +    """Injects the value of the attribute as-is without flag. Metadata modifier for :func:`dataclasses.field`."""
>>>> +    return {**metadata, META_VALUE_ONLY: True}
>>>
>>> These methods, on the other hand, are used outside this module, so it
>>> makes sense to have them outside Params. It could be better to have
>>> them inside as static methods, as they're closely related. Looking at
>>> how they're used, we should unite the imports:
>>> 1. In testpmd_shell, they're imported directly (from framework.params
>>> import long)
>>> 2. In sut_node, just the params module is imported (from framework
>>> import params and then accessed via it: metadata=params.short("l"))
>>>
>> Having a shorter version may look less verbose. I agree that we can make
>> everything a static method of Params, but then having to do Params.short
>> etc everytime will make it look more verbose. So what option do we
>> prefer? The functions do belong to the params module nonetheless, and
>> therefore are meant to be used in conjunction with the Params class.
>>
> 
> When I first saw the code, I liked the usage in sut_node better, e.g.:
> prefix: str = field(metadata=params.long("file-prefix")). I think this
> is because it's obvious where the function comes from. I'd do the
> longer version because I think most people are just going to glance at
> the code when they want to know how to properly implement an argument
> so the explicit nature could help with understanding how it should be
> done.

Ack.

>>> If we move these to Params, we could import Params and use them
>>> similarly to 2. Not sure which one is better.
>>>
>>
>>
>>>> +def field_mixins(*mixins: Mixin, metadata: dict[str, Any] = {}) -> dict[str, Any]:
>>>
>>> Any reason why mixins are plural? I've only seen this used with one
>>> argument, do you anticipate we'd need to use more than one? We could
>>> make this singular and if we ever need to do two things, we could just
>>> pass a function that does those two things (or calls two different
>>> functions). Also, I'd just rename the mixin the func or something like
>>> that.
>>>
>>> The default of an argument should not be mutable, here's a quick
>>> explanation: https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments
>>
>>
>> Indeed the reason for which I create dictionaries, as I am treating them
>> as read only. I wanted to avoid to bloat the code with lots of `is None`
>> checks. But we can sacrifice this optimization for better code.
>>
> 
> This would be the only place where we'd do the check, as I don't think
> we need the metadata argument in any of the other functions - those
> seem to be mutually exclusive, but maybe they're not? In any case,
> we'd need to fix this, I don't think treating them as read-only avoids
> the problem.
> 

They are not mutually exclusive. But thinking of it we can spare every 
problem with having to chain "metadata" by letting the user do it 
through the use of the pipe operator.

>> Here we
>> are just chaining them for reusability. Do you have any better name in mind?
>>
> 
> I don't know, so let's brainstorm a bit. Let's start with the usage:
> portmask: int | None = field(default=None, metadata=field_mixins(hex))
> 
> Here it's not clear at all why it's called field_mixins, at least
> compared to the other functions which are not called mixins. I guess
> the other functions are predefined option mixins whereas we're
> supplying our own value mixins here. I also noticed that there's a bit
> of an inconsistency with the naming. The basic functions (long etc.)
> don't have the "field_" prefix, but this one does. Maybe a better name
> would be custom_mixins? Or value_mixins? Or custom_value? Or maybe
> convert_value? I like the last one:
> portmask: int | None = field(default=None, metadata=convert_value(hex))
> metadata=params.convert_value(_port_to_pci,
> metadata=params.multiple(params.short("a"))), # in sut_node
> 
> I think this is easier to grasp. I'm thinking about whether we need to
> have mixin(s) in the name and I don't think it adds much. If I'm a
> developer, I'm looking at these functions and I stumble upon
> convert_value, what I'm thinking is "Nice, I can do some conversion on
> the values I pass, how do I do that?", then I look at the signature
> and find out that I expect, that is I need to pass a function (or
> multiple function if I want to). I guess this comes down to the
> function name (field_mixins) not conveying what it's doing, rather
> what you're passing to it.
> 
> So my conclusion from this brainstorming is that a better name would
> be convert_value. :-)
> 
> Also, unrelated, but the context is lost. Another thing I just noticed
> is in the docstring:
> The ``metadata`` keyword argument can be used to chain metadata
> modifiers together.
> 
> We're missing the Args: section in all of the docstrings (where we
> could put the above). Also the Returns: section.

Sure, we can do convert_value. I am honestly not too fussed about 
naming, and your proposal makes more sense. And as above, we can spare 
the whole metadata problem. Using your example:

   metadata=params.short("a") | params.multiple()

>>>> +    return {**metadata, META_MIXINS: mixins}
>>>
>>> metadata | {META_MIXINS: mixins} also creates a new dictionary with
>>> values from both and I think that would be more readable (since it's
>>> mentioned in docs:
>>> https://docs.python.org/3/library/stdtypes.html#mapping-types-dict).
>>
>> If we were to use `None` as default to the arguments, then this would no
>> longer be needed. But great shout, wasn't aware of this feature added in
>> 3.9.
>>
> 
> It wouldn't? We'd still have to merge the dicts when metadata is not None, no?
> 
> <snip>


^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 2/6] dts: use Params for interactive shells
  2024-04-10  9:34       ` Luca Vizzarro
@ 2024-04-10  9:58         ` Juraj Linkeš
  0 siblings, 0 replies; 159+ messages in thread
From: Juraj Linkeš @ 2024-04-10  9:58 UTC (permalink / raw)
  To: Luca Vizzarro
  Cc: Jeremy Spewock, dev, Jack Bond-Preston, Honnappa Nagarahalli

On Wed, Apr 10, 2024 at 11:34 AM Luca Vizzarro <Luca.Vizzarro@arm.com> wrote:
>
> On 09/04/2024 15:56, Juraj Linkeš wrote:
> > On Thu, Mar 28, 2024 at 5:48 PM Jeremy Spewock <jspewock@iol.unh.edu> wrote:
> >>
> >> I'm not sure if allowing None should be the solution for these shells
> >> as opposed to just supplying an empty parameter object. Maybe
> >> something that could be done is the factory method in sut_node allows
> >> it to be None, but when it comes time to make the abstract shell it
> >> creates an empty one if it doesn't exist, or the init method makes
> >> this an optional parameter but creates it if it doesn't exist.
> >>
> >> I suppose this logic would have to exist somewhere because the
> >> parameters aren't always required, it's just a question of where we
> >> should put it and if we should just assume that the interactive shell
> >> class just knows how to accept some parameters and put them into the
> >> shell. I would maybe leave this as something that cannot be None and
> >> handle it outside of the shell, but I'm not sure it's something really
> >> required and I don't have a super strong opinion on it.
> >>
> >
> > I think this is an excellent idea. An empty Params (or StrParams or
> > EalParams if we want to make Params truly abstract and thus not
> > instantiable) would work as the default value and it would be an
> > elegant solution since dev will only pass non-empty Params.
> >
>
> I left it as generic as it could get as I honestly couldn't grasp the
> full picture. I am really keen to ditch everything else and use an empty
> Params object instead for defaults. And as Juraj said, if I am making
> Params a true abstract object, then it'd be picking one of the Params
> subclasses mentioned above.
>
> I guess EalParams could only be used with shells are that sure to be
> DPDK apps, and I presume that's only TestPmdShell for now.
>
> >>> +        from framework.testbed_model.sut_node import EalParameters
> >>> +
> >>> +        assert isinstance(self._app_args, EalParameters)
> >>> +
> >>> +        if isinstance(self._app_args.app_params, StrParams):
> >>> +            self._app_args.app_params.value += " -i --mask-event intr_lsc"
> >>> +
> >>> +        self.number_of_ports = len(self._app_args.ports) if self._app_args.ports is not None else 0
> >>
> >> I we should override the _app_args parameter in the testpmd shell to
> >> always be EalParameters instead of doing this import and assertion.
> >> It's a DPDK app, so we will always need EalParameters anyway, so we
> >> might as well put that as our typehint to start off as what we expect
> >> instead.
> >>
> >> The checking of an instance of StrParams also feels a little strange
> >> here, it might be more ideal if we could just add the parameters
> >> without this check. Maybe something we can do, just because these
> >> parameters are meant to be CLI commands anyway and will be rendered as
> >> a string, is replace the StrParams object with a method on the base
> >> Params dataclass that allows you to just add any string as a value
> >> only field. Then, we don't have to bother validating anything about
> >> the app parameters and we don't care what they are, we can just add a
> >> string to them of new parameters.
> >>
> >> I think this is something that likely also gets solved when you
> >> replace this with testpmd parameters, but it might be a good way to
> >> make the Params object more versatile in general so that people can
> >> diverge and add their own flags to it if needed.
>
> Jeremy, I agree this is not ideal. Although this was meant only to be
> transitionary for the next commit (as you say it gets resolved with
> TestPmdParams). But I agree with you that we can integrate the StrParams
> facility within Params. This means no longer making Params a true
> abstract class though, which is something I can live with, especially if
> we can make it simpler.
>

No problem with it not being a true abstract class if it's not going
to have abstract methods/properties. I guess making it instantiable
actually makes sense, since it's always going to be empty (as there
are no fields), which should make the usage mostly error-free.

> > I'd say this is done because of circular imports. If so, we could move
> > EalParameters to params.py, that should help. And when we're at it,
> > either rename it to EalParams or rename the other classes to use the
> > whole word.
>
> Yeah, the circular imports are the main problem indeed. I considered
> refactoring but noticed it'd take more than just moving EalParam(eter)s
> to params.py. Nonetheless, keen to make a bigger refactoring to make
> this happen.

Please do. My thinking was if we made params.py standalone we could
import from it anywhere but I guess it's not that simple. :-) In any
case, refactoring is always welcome - please put moved files into a
separate commit.

> >>> +
> >>>           super()._start_application(get_privileged_command)
> >>>
> >>>       def start(self, verify: bool = True) -> None:
> >>   <snip>
> >>> @@ -134,7 +136,7 @@ def create_interactive_shell(
> >>>           shell_cls: Type[InteractiveShellType],
> >>>           timeout: float,
> >>>           privileged: bool,
> >>> -        app_args: str,
> >>> +        app_args: Params | None,
> >>
> >> This also falls in line with what I was saying before about where this
> >> logic is handled. This was made to always be a string originally
> >> because by this point it being optional was already handled by the
> >> sut_node.create_interactive_shell() and we should have some kind of
> >> value here (even if that value is an empty parameters dataclass) to
> >> pass into the application. It sort of semantically doesn't really
> >> change much, but at this point it might as well not be None, so we can
> >> simplify this type.
> Ack.
> >>>       ) -> InteractiveShellType:
> >>>           """Factory for interactive session handlers.
> >>>
> <snip>
> >>> +@dataclass(kw_only=True)
> >>> +class EalParameters(Params):
> >>>       """The environment abstraction layer parameters.
> >>>
> >>>       The string representation can be created by converting the instance to a string.
> >>>       """
> >>>
> >>> -    def __init__(
> >>> -        self,
> >>> -        lcore_list: LogicalCoreList,
> >>> -        memory_channels: int,
> >>> -        prefix: str,
> >>> -        no_pci: bool,
> >>> -        vdevs: list[VirtualDevice],
> >>> -        ports: list[Port],
> >>> -        other_eal_param: str,
> >>> -    ):
> >>> -        """Initialize the parameters according to inputs.
> >>> -
> >>> -        Process the parameters into the format used on the command line.
> >>> +    lcore_list: LogicalCoreList = field(metadata=params.short("l"))
> >>> +    """The list of logical cores to use."""
> >>>
> >>> -        Args:
> >>> -            lcore_list: The list of logical cores to use.
> >>> -            memory_channels: The number of memory channels to use.
> >>> -            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
> >>> -            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
> >>> -            vdevs: Virtual devices, e.g.::
> >>> +    memory_channels: int = field(metadata=params.short("n"))
> >>> +    """The number of memory channels to use."""
> >>>
> >>> -                vdevs=[
> >>> -                    VirtualDevice('net_ring0'),
> >>> -                    VirtualDevice('net_ring1')
> >>> -                ]
> >>> -            ports: The list of ports to allow.
> >>> -            other_eal_param: user defined DPDK EAL parameters, e.g.:
> >>> -                ``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._ports = " ".join(f"-a {port.pci}" for port in ports)
> >>> -        self._other_eal_param = other_eal_param
> >>> -
> >>> -    def __str__(self) -> str:
> >>> -        """Create the EAL string."""
> >>> -        return (
> >>> -            f"{self._lcore_list} "
> >>> -            f"{self._memory_channels} "
> >>> -            f"{self._prefix} "
> >>> -            f"{self._no_pci} "
> >>> -            f"{self._vdevs} "
> >>> -            f"{self._ports} "
> >>> -            f"{self._other_eal_param}"
> >>> -        )
> >>> +    prefix: str = field(metadata=params.long("file-prefix"))
> >>> +    """Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``."""
> >>
> >> Just a small note on docstrings, I believe generally in DTS we use the
> >> google docstring
> >> (https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
> >> format where it applies with some small differences. Because these
> >> attributes aren't class vars however, I believe they should be in the
> >> docstring for the class in the `Attributes` section. I generally have
> >> trouble remembering exactly how it should look, but Juraj documented
> >> it in `doc/guides/tools/dts.rst`
> >>
> >
> > I noticed this right away. Here's the bullet point that applies:
> >
> > * The ``dataclass.dataclass`` decorator changes how the attributes are
> > processed.
> >    The dataclass attributes which result in instance variables/attributes
> >    should also be recorded in the ``Attributes:`` section.
> >
>
> I truly did not even for a second recognise the distinction. But this
> explains a lot. So the idea here is to move every documented field as an
> attribute in the main class docstring?
>

Yes. You can look at the dataclasses in config/_init__.py to get an
idea on what it should look like.

> >>> +
> >>> +    no_pci: params.Option
> >>> +    """Switch to disable PCI bus e.g.: ``no_pci=True``."""
> >> <snip>
> >>
> >>> --
> >>> 2.34.1
> >>>
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 1/6] dts: add parameters data structure
  2024-04-10  9:51         ` Luca Vizzarro
@ 2024-04-10 10:04           ` Juraj Linkeš
  0 siblings, 0 replies; 159+ messages in thread
From: Juraj Linkeš @ 2024-04-10 10:04 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Jack Bond-Preston, Honnappa Nagarahalli

On Wed, Apr 10, 2024 at 11:51 AM Luca Vizzarro <Luca.Vizzarro@arm.com> wrote:
>
> On 10/04/2024 10:15, Juraj Linkeš wrote:
> >>>> +
> >>>> +def value_only(metadata: dict[str, Any] = {}) -> dict[str, Any]:
> >>>> +    """Injects the value of the attribute as-is without flag. Metadata modifier for :func:`dataclasses.field`."""
> >>>> +    return {**metadata, META_VALUE_ONLY: True}
> >>>
> >>> These methods, on the other hand, are used outside this module, so it
> >>> makes sense to have them outside Params. It could be better to have
> >>> them inside as static methods, as they're closely related. Looking at
> >>> how they're used, we should unite the imports:
> >>> 1. In testpmd_shell, they're imported directly (from framework.params
> >>> import long)
> >>> 2. In sut_node, just the params module is imported (from framework
> >>> import params and then accessed via it: metadata=params.short("l"))
> >>>
> >> Having a shorter version may look less verbose. I agree that we can make
> >> everything a static method of Params, but then having to do Params.short
> >> etc everytime will make it look more verbose. So what option do we
> >> prefer? The functions do belong to the params module nonetheless, and
> >> therefore are meant to be used in conjunction with the Params class.
> >>
> >
> > When I first saw the code, I liked the usage in sut_node better, e.g.:
> > prefix: str = field(metadata=params.long("file-prefix")). I think this
> > is because it's obvious where the function comes from. I'd do the
> > longer version because I think most people are just going to glance at
> > the code when they want to know how to properly implement an argument
> > so the explicit nature could help with understanding how it should be
> > done.
>
> Ack.
>
> >>> If we move these to Params, we could import Params and use them
> >>> similarly to 2. Not sure which one is better.
> >>>
> >>
> >>
> >>>> +def field_mixins(*mixins: Mixin, metadata: dict[str, Any] = {}) -> dict[str, Any]:
> >>>
> >>> Any reason why mixins are plural? I've only seen this used with one
> >>> argument, do you anticipate we'd need to use more than one? We could
> >>> make this singular and if we ever need to do two things, we could just
> >>> pass a function that does those two things (or calls two different
> >>> functions). Also, I'd just rename the mixin the func or something like
> >>> that.
> >>>
> >>> The default of an argument should not be mutable, here's a quick
> >>> explanation: https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments
> >>
> >>
> >> Indeed the reason for which I create dictionaries, as I am treating them
> >> as read only. I wanted to avoid to bloat the code with lots of `is None`
> >> checks. But we can sacrifice this optimization for better code.
> >>
> >
> > This would be the only place where we'd do the check, as I don't think
> > we need the metadata argument in any of the other functions - those
> > seem to be mutually exclusive, but maybe they're not? In any case,
> > we'd need to fix this, I don't think treating them as read-only avoids
> > the problem.
> >
>
> They are not mutually exclusive. But thinking of it we can spare every
> problem with having to chain "metadata" by letting the user do it
> through the use of the pipe operator.
>

Sounds good.

> >> Here we
> >> are just chaining them for reusability. Do you have any better name in mind?
> >>
> >
> > I don't know, so let's brainstorm a bit. Let's start with the usage:
> > portmask: int | None = field(default=None, metadata=field_mixins(hex))
> >
> > Here it's not clear at all why it's called field_mixins, at least
> > compared to the other functions which are not called mixins. I guess
> > the other functions are predefined option mixins whereas we're
> > supplying our own value mixins here. I also noticed that there's a bit
> > of an inconsistency with the naming. The basic functions (long etc.)
> > don't have the "field_" prefix, but this one does. Maybe a better name
> > would be custom_mixins? Or value_mixins? Or custom_value? Or maybe
> > convert_value? I like the last one:
> > portmask: int | None = field(default=None, metadata=convert_value(hex))
> > metadata=params.convert_value(_port_to_pci,
> > metadata=params.multiple(params.short("a"))), # in sut_node
> >
> > I think this is easier to grasp. I'm thinking about whether we need to
> > have mixin(s) in the name and I don't think it adds much. If I'm a
> > developer, I'm looking at these functions and I stumble upon
> > convert_value, what I'm thinking is "Nice, I can do some conversion on
> > the values I pass, how do I do that?", then I look at the signature
> > and find out that I expect, that is I need to pass a function (or
> > multiple function if I want to). I guess this comes down to the
> > function name (field_mixins) not conveying what it's doing, rather
> > what you're passing to it.
> >
> > So my conclusion from this brainstorming is that a better name would
> > be convert_value. :-)
> >
> > Also, unrelated, but the context is lost. Another thing I just noticed
> > is in the docstring:
> > The ``metadata`` keyword argument can be used to chain metadata
> > modifiers together.
> >
> > We're missing the Args: section in all of the docstrings (where we
> > could put the above). Also the Returns: section.
>
> Sure, we can do convert_value. I am honestly not too fussed about
> naming, and your proposal makes more sense.

Ok. I like to think a lot about these names because I think it would
save a considerable amount of time for future developers.

> And as above, we can spare
> the whole metadata problem. Using your example:
>
>    metadata=params.short("a") | params.multiple()
>

I see what you meant, this is awesome. Intuitive, understandable,
concise and easy to use.

> >>>> +    return {**metadata, META_MIXINS: mixins}
> >>>
> >>> metadata | {META_MIXINS: mixins} also creates a new dictionary with
> >>> values from both and I think that would be more readable (since it's
> >>> mentioned in docs:
> >>> https://docs.python.org/3/library/stdtypes.html#mapping-types-dict).
> >>
> >> If we were to use `None` as default to the arguments, then this would no
> >> longer be needed. But great shout, wasn't aware of this feature added in
> >> 3.9.
> >>
> >
> > It wouldn't? We'd still have to merge the dicts when metadata is not None, no?
> >
> > <snip>
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 3/6] dts: add testpmd shell params
  2024-04-09 16:37   ` Juraj Linkeš
@ 2024-04-10 10:49     ` Luca Vizzarro
  2024-04-10 13:17       ` Juraj Linkeš
  0 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-04-10 10:49 UTC (permalink / raw)
  To: Juraj Linkeš, Jeremy Spewock
  Cc: dev, Jack Bond-Preston, Honnappa Nagarahalli

On 09/04/2024 17:37, Juraj Linkeš wrote:
> As Jeremy pointed out, going forward, this is likely to become bloated
> and moving it to params.py (for example) may be better.
> 
> There's a lot of testpmd args here. I commented on the implementation
> of some of them. I didn't verify that the actual values match the docs
> or, god forbid, tested all of it. :-) Doing that as we start using
> them is going to be good enough.

It is indeed a lot of args. I double checked most of them, so it should 
be mostly correct, but unfortunately I am not 100% sure. I did notice 
discrepancies between the docs and the source code of testpmd too. 
Although not ideal, I am inclining to update the definitions whenever a 
newly implemented test case hits a roadblock.

One thing that I don't remember if I mentioned so far, is the "XYPair". 
You see --flag=X,[Y] in the docs, but I am sure to have read somewhere 
this is potentially just a comma-separated multiple value.

> On Tue, Mar 26, 2024 at 8:04 PM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>>
>> Implement all the testpmd shell parameters into a data structure.
>>
>> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
>> Reviewed-by: Jack Bond-Preston <jack.bond-preston@arm.com>
>> Reviewed-by: Honnappa Nagarahalli <honnappa.nagarahalli@arm.com>
>> ---
>>   dts/framework/remote_session/testpmd_shell.py | 633 +++++++++++++++++-
>>   1 file changed, 615 insertions(+), 18 deletions(-)
>>
>> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
>> index db3abb7600..a823dc53be 100644
>> --- a/dts/framework/remote_session/testpmd_shell.py
>> +++ b/dts/framework/remote_session/testpmd_shell.py
> 
> <snip>
> 
>> +@str_mixins(bracketed, comma_separated)
>> +class TestPmdRingNUMAConfig(NamedTuple):
>> +    """Tuple associating DPDK port, direction of the flow and NUMA socket."""
> 
> Is there any particular order for these various classes?

No, there is no actual order, potential dependencies aside.

>> +
>> +    port: int
>> +    direction: TestPmdFlowDirection
>> +    socket: int
>> +
>> +
> 
> <snip>
> 
>> +@dataclass(kw_only=True)
>> +class TestPmdTXOnlyForwardingMode(Params):
> 
> The three special forwarding modes should really be moved right after
> TestPmdForwardingModes. Do we actually need these three in
> TestPmdForwardingModes? Looks like we could just remove those from
> TestPmdForwardingModes since they have to be passed separately, not as
> that Enum.

Can move and no we don't really need them in TestPmdForwardingModes, 
they can be hardcoded in their own special classes.

>> +    __forward_mode: Literal[TestPmdForwardingModes.txonly] = field(
>> +        default=TestPmdForwardingModes.txonly, init=False, metadata=long("forward-mode")
>> +    )
> 
> I guess this is here so that "--forward-mode=txonly" gets rendered,
> right? Why the two underscored? Is that because we want to hammer home
> the fact that this is init=False, a kind of internal field? I'd like
> to make it like the other fields, without any underscores (or maybe
> just one underscore), and documented (definitely documented).
> If we remove txonly from the Enum, we could just have the string value
> here. The Enums are mostly useful to give users the proper range of
> values.
> 

Correct and correct. A double underscore would ensure no access to this 
field, which is fixed and only there for rendering purposes... (also the 
developer doesn't get a hint from the IDE, at least not on VS code) and 
in the case of TestPmdForwardingModes it would remove a potential 
conflict. It can definitely be documented though.

>> +    multi_flow: Option = field(default=None, metadata=long("txonly-multi-flow"))
>> +    """Generate multiple flows."""
>> +    segments_length: XYPair | None = field(default=None, metadata=long("txpkts"))
>> +    """Set TX segment sizes or total packet length."""
>> +
>> +
>> +@dataclass(kw_only=True)
>> +class TestPmdFlowGenForwardingMode(Params):
>> +    __forward_mode: Literal[TestPmdForwardingModes.flowgen] = field(
>> +        default=TestPmdForwardingModes.flowgen, init=False, metadata=long("forward-mode")
>> +    )
>> +    clones: int | None = field(default=None, metadata=long("flowgen-clones"))
>> +    """Set the number of each packet clones to be sent. Sending clones reduces host CPU load on
>> +    creating packets and may help in testing extreme speeds or maxing out Tx packet performance.
>> +    N should be not zero, but less than ‘burst’ parameter.
>> +    """
>> +    flows: int | None = field(default=None, metadata=long("flowgen-flows"))
>> +    """Set the number of flows to be generated, where 1 <= N <= INT32_MAX."""
>> +    segments_length: XYPair | None = field(default=None, metadata=long("txpkts"))
>> +    """Set TX segment sizes or total packet length."""
>> +
>> +
>> +@dataclass(kw_only=True)
>> +class TestPmdNoisyForwardingMode(Params):
>> +    __forward_mode: Literal[TestPmdForwardingModes.noisy] = field(
>> +        default=TestPmdForwardingModes.noisy, init=False, metadata=long("forward-mode")
>> +    )
> 
> Are both of __forward_mode and forward_mode needed because we need to
> render both?

Yes, this would render as `--forward-mode=noisy --noisy-forward-mode=io` 
using IO as example.

>> +    forward_mode: (
>> +        Literal[
>> +            TestPmdForwardingModes.io,
>> +            TestPmdForwardingModes.mac,
>> +            TestPmdForwardingModes.macswap,
>> +            TestPmdForwardingModes.fivetswap,
>> +        ]
>> +        | None
> 
> Is there a difference between using union (TestPmdForwardingModes.io |
> TestPmdForwardingModes.mac etc.) and Literal?

TestPmdForwardingModes.io etc are literals and mypy complains:

error: Invalid type: try using Literal[TestPmdForwardingModes.io] 
instead?  [misc]

Therefore they need to be wrapped in Literal[..]

Literal[A, B] is the equivalent of Union[Literal[A], Literal[B]]

So this ultimately renders as Union[Lit[io], Lit[mac], Lit[macswap], 
Lit[fivetswap], None]. So it's really a matter of conciseness, by using 
Literal[A, ..], vs intuitiveness, by using Literal[A] | Literal[..] | ..

Which one would we prefer?

>> +@dataclass
>> +class TestPmdDisableRSS(Params):
>> +    """Disable RSS (Receive Side Scaling)."""
> 
> Let's put the explanation/reminder of what RSS stands for to either
> all three classes or none of them.
> 

Ack.
>> +    rss: TestPmdDisableRSS | TestPmdSetRSSIPOnly | TestPmdSetRSSUDP | None = None
>> +    """RSS option setting.
>> +
>> +    The value can be one of:
>> +    * :class:`TestPmdDisableRSS`, to disable RSS
>> +    * :class:`TestPmdSetRSSIPOnly`, to set RSS for IPv4/IPv6 only
>> +    * :class:`TestPmdSetRSSUDP`, to set RSS for IPv4/IPv6 and UDP
>> +    """
> 
> Have you thought about making an Enum where values would be these
> classes? That could simplify things a bit for users if it works.

It would be lovely to have classes as enum values, and I thought of it 
thinking of other languages like Rust. Not sure this is possible in 
Python. Are you suggesting to pass a class type as a value? In the hope 
that doing:

   TestPmdRSS.Disable()

could work? As this wouldn't. What works instead is:

   TestPmdRSS.Disable.value()

Which is somewhat ugly. Maybe I could modify the behaviour of the enum 
to return the underlying value instead of a reference to the field.

Do you have any better ideas?

>> +
>> +    forward_mode: (
>> +        Literal[
>> +            TestPmdForwardingModes.io,
>> +            TestPmdForwardingModes.mac,
>> +            TestPmdForwardingModes.macswap,
>> +            TestPmdForwardingModes.rxonly,
>> +            TestPmdForwardingModes.csum,
>> +            TestPmdForwardingModes.icmpecho,
>> +            TestPmdForwardingModes.ieee1588,
>> +            TestPmdForwardingModes.fivetswap,
>> +            TestPmdForwardingModes.shared_rxq,
>> +            TestPmdForwardingModes.recycle_mbufs,
>> +        ]
> 
> This could result in just TestPmdForwardingModes | the rest if we
> remove the compound fw modes from TestPmdForwardingModes. Maybe we
> could rename TestPmdForwardingModes to TestPmdSimpleForwardingModes or
> something at that point.

Yes, good idea.

>> +        | TestPmdFlowGenForwardingMode
>> +        | TestPmdTXOnlyForwardingMode
>> +        | TestPmdNoisyForwardingMode
>> +        | None
>> +    ) = TestPmdForwardingModes.io
>> +    """Set the forwarding mode.
> 
> <snip>
> 
>> +    mempool_allocation_mode: (
>> +        Literal[
>> +            TestPmdMempoolAllocationMode.native,
>> +            TestPmdMempoolAllocationMode.xmem,
>> +            TestPmdMempoolAllocationMode.xmemhuge,
>> +        ]
>> +        | TestPmdAnonMempoolAllocationMode
>> +        | None
> 
> This looks similar to fw modes, maybe the same applies here as well.

Ack.

>> +    ) = field(default=None, metadata=long("mp-alloc"))
>> +    """Select mempool allocation mode.
>> +
>> +    The value can be one of:
>> +    * :attr:`TestPmdMempoolAllocationMode.native`
>> +    * :class:`TestPmdAnonMempoolAllocationMode`
>> +    * :attr:`TestPmdMempoolAllocationMode.xmem`
>> +    * :attr:`TestPmdMempoolAllocationMode.xmemhuge`
>> +    """


^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 4/6] dts: use testpmd params for scatter test suite
  2024-04-09 19:12   ` Juraj Linkeš
@ 2024-04-10 10:53     ` Luca Vizzarro
  2024-04-10 13:18       ` Juraj Linkeš
  0 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-04-10 10:53 UTC (permalink / raw)
  To: Juraj Linkeš, Jeremy Spewock
  Cc: dev, Jack Bond-Preston, Honnappa Nagarahalli

On 09/04/2024 20:12, Juraj Linkeš wrote:
>> @@ -104,16 +108,15 @@ def pmd_scatter(self, mbsize: int) -> None:
>>           """
>>           testpmd = self.sut_node.create_interactive_shell(
>>               TestPmdShell,
>> -            app_parameters=StrParams(
>> -                "--mbcache=200 "
>> -                f"--mbuf-size={mbsize} "
>> -                "--max-pkt-len=9000 "
>> -                "--port-topology=paired "
>> -                "--tx-offloads=0x00008000"
>> +            app_parameters=TestPmdParameters(
>> +                forward_mode=TestPmdForwardingModes.mac,
>> +                mbcache=200,
>> +                mbuf_size=[mbsize],
>> +                max_pkt_len=9000,
>> +                tx_offloads=0x00008000,
>>               ),
>>               privileged=True,
>>           )
>> -        testpmd.set_forward_mode(TestPmdForwardingModes.mac)
> 
> Jeremy, does this change the test? Instead of configuring the fw mode
> after starting testpmd, we're starting testpmd with fw mode
> configured.

I am not Jeremy (please Jeremy still reply), but we discussed this on 
Slack. Reading through the testpmd source code, setting arguments like 
forward-mode in the command line, is the exact equivalent of calling 
`set forward mode` right after start-up. So it is equivalent in theory.

> If not, we should remove the testpmd.set_forward_mode method, as it's
> not used anymore.

Could there be test cases that change the forward mode multiple times in 
the same shell, though? As this could still be needed to cover this.

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 5/6] dts: add statefulness to InteractiveShell
  2024-04-10  6:53     ` Juraj Linkeš
@ 2024-04-10 11:27       ` Luca Vizzarro
  2024-04-10 13:35         ` Juraj Linkeš
  0 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-04-10 11:27 UTC (permalink / raw)
  To: Juraj Linkeš, Jeremy Spewock
  Cc: dev, Jack Bond-Preston, Honnappa Nagarahalli

On 10/04/2024 07:53, Juraj Linkeš wrote:
> I have a general question. What are these changes for? Do you
> anticipate us needing this in the future? Wouldn't it be better to add
> it only when we need it?

It's been sometime since we raised this task internally. This patch and 
the next one arise from some survey done on old DTS test cases.
Unfortunately, I can't pinpoint.

Specifically for this patch though, the timeout bit is useful in 
conjunction with the related change in the next. Instead of giving an 
optional timeout argument to all the commands where we may want to 
change it, aren't we better off with providing a facility to temporarily 
change this for the current scope?

> 
> On Thu, Mar 28, 2024 at 5:48 PM Jeremy Spewock <jspewock@iol.unh.edu> wrote:
>>
>> On Tue, Mar 26, 2024 at 3:04 PM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>> <snip>
>>> diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
>>> index a2c7b30d9f..5d80061e8d 100644
>>> --- a/dts/framework/remote_session/interactive_shell.py
>>> +++ b/dts/framework/remote_session/interactive_shell.py
>>> @@ -41,8 +41,10 @@ class InteractiveShell(ABC):
>>>       _stdout: channel.ChannelFile
>>>       _ssh_channel: Channel
>>>       _logger: DTSLogger
>>> +    __default_timeout: float
>>
>> Only single underscores are used for other private variables, probably
>> better to keep that consistent with this one.
>>
> 
> I agree, I don't see a reason for the double underscore.

Ack.

> 
>>>       _timeout: float
>>>       _app_args: Params | None
>>> +    _is_privileged: bool = False
>> <snip>
>>> 2.34.1
>>>


^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 6/6] dts: add statefulness to TestPmdShell
  2024-04-10  7:41     ` Juraj Linkeš
@ 2024-04-10 11:35       ` Luca Vizzarro
  2024-04-11 10:30         ` Juraj Linkeš
  0 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-04-10 11:35 UTC (permalink / raw)
  To: Juraj Linkeš, Jeremy Spewock
  Cc: dev, Jack Bond-Preston, Honnappa Nagarahalli

On 10/04/2024 08:41, Juraj Linkeš wrote:
>> <snip>
>>> @@ -723,7 +731,13 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
>>>           if self._app_args.app_params is None:
>>>               self._app_args.app_params = TestPmdParameters()
>>>
>>> -        self.number_of_ports = len(self._app_args.ports) if self._app_args.ports is not None else 0
>>> +        assert isinstance(self._app_args.app_params, TestPmdParameters)
>>> +
>>
>> This is tricky because ideally we wouldn't have the assertion here,
>> but I understand why it is needed because Eal parameters have app args
>> which can be any instance of params. I'm not sure of the best way to
>> solve this, because making testpmd parameters extend from eal would
>> break the general scheme that you have in place, and having an
>> extension of EalParameters that enforces this app_args is
>> TestPmdParameters would solve the issues, but might be a little
>> clunky. Is there a way we can use a generic to get python to just
>> understand that, in this case, this will always be TestPmdParameters?
>> If not I might prefer making a private class where this is
>> TestPmdParameters, just because there aren't really any other
>> assertions that we use elsewhere and an unexpected exception from this
>> (even though I don't think that can happen) could cause people some
>> issues.
>>
>> It might be the case that an assertion is the easiest way to deal with
>> it though, what do you think?
>>
> 
> We could change the signature (just the type of app_args) of the init
> method - I think we should be able to create a type that's
> EalParameters with .app_params being TestPmdParameters or None. The
> init method would just call super().
> 
> Something like the above is basically necessary with inheritance where
> subclasses are all extensions (not just implementations) of the
> superclass (having differences in API).
> 

I believe this is indeed a tricky one. But, unfortunately, I am not 
understanding the solution that is being proposed. To me, it just feels 
like using a generic factory like:

   self.sut_node.create_interactive_shell(..)

is one of the reasons to bring in the majority of these complexities.

What do you mean by creating this new type that combines EalParams and 
TestPmdParams?

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 6/6] dts: add statefulness to TestPmdShell
  2024-04-10  7:50   ` Juraj Linkeš
@ 2024-04-10 11:37     ` Luca Vizzarro
  0 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-04-10 11:37 UTC (permalink / raw)
  To: Juraj Linkeš; +Cc: dev, Jack Bond-Preston, Honnappa Nagarahalli

On 10/04/2024 08:50, Juraj Linkeš wrote:
> On Tue, Mar 26, 2024 at 8:04 PM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>>
>> This commit provides a state container for TestPmdShell. It currently
>> only indicates whether the packet forwarding has started
>> or not, and the number of ports which were given to the shell.
>>
> 
> A reminder, the commit message should explain why we're doing this
> change, not what the change is.
> 
>> This also fixes the behaviour of `wait_link_status_up` to use the
>> command timeout as inherited from InteractiveShell.
>>
>> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
>> Reviewed-by: Jack Bond-Preston <jack.bond-preston@arm.com>
>> Reviewed-by: Honnappa Nagarahalli <honnappa.nagarahalli@arm.com>
>> ---
>>   dts/framework/remote_session/testpmd_shell.py | 41 +++++++++++++------
>>   1 file changed, 28 insertions(+), 13 deletions(-)
>>
>> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
>> index a823dc53be..ea1d254f86 100644
>> --- a/dts/framework/remote_session/testpmd_shell.py
>> +++ b/dts/framework/remote_session/testpmd_shell.py
>> @@ -678,19 +678,27 @@ def __str__(self) -> str:
>>           return self.pci_address
>>
>>
>> +@dataclass(slots=True)
>> +class TestPmdState:
>> +    """Session state container."""
>> +
>> +    #:
>> +    packet_forwarding_started: bool = False
> 
> The same question as in the previous patch, do you anticipate this
> being needed and should we add this only when it's actually used?
> 

As answered in the previous patch. We can always drop it and do it as 
needed of course.

>> +
>> +    #: The number of ports which were allowed on the command-line when testpmd was started.
>> +    number_of_ports: int = 0
>> +
>> +
>>   class TestPmdShell(InteractiveShell):
>>       """Testpmd interactive shell.
>>
>>       The testpmd shell users should never use
>>       the :meth:`~.interactive_shell.InteractiveShell.send_command` method directly, but rather
>>       call specialized methods. If there isn't one that satisfies a need, it should be added.
>> -
>> -    Attributes:
>> -        number_of_ports: The number of ports which were allowed on the command-line when testpmd
>> -            was started.
>>       """
>>
>> -    number_of_ports: int
>> +    #: Current state
>> +    state: TestPmdState = TestPmdState()
> 
> Assigning a value makes this a class variable, shared across all
> instances. This should be initialized in __init__().
> 
> But do we actually want to do this via composition? We'd need to
> access the attributes via .state all the time and I don't really like
> that. We could just put them into TestPmdShell directly, initializing
> them in __init__().

No problem. I separated them in fear of bloating TestPmdShell. But I 
agree on the bother of adding .state

>>
>>       #: The path to the testpmd executable.
>>       path: ClassVar[PurePath] = PurePath("app", "dpdk-testpmd")


^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 3/6] dts: add testpmd shell params
  2024-04-10 10:49     ` Luca Vizzarro
@ 2024-04-10 13:17       ` Juraj Linkeš
  0 siblings, 0 replies; 159+ messages in thread
From: Juraj Linkeš @ 2024-04-10 13:17 UTC (permalink / raw)
  To: Luca Vizzarro
  Cc: Jeremy Spewock, dev, Jack Bond-Preston, Honnappa Nagarahalli

On Wed, Apr 10, 2024 at 12:49 PM Luca Vizzarro <Luca.Vizzarro@arm.com> wrote:
>
> On 09/04/2024 17:37, Juraj Linkeš wrote:
> > As Jeremy pointed out, going forward, this is likely to become bloated
> > and moving it to params.py (for example) may be better.
> >
> > There's a lot of testpmd args here. I commented on the implementation
> > of some of them. I didn't verify that the actual values match the docs
> > or, god forbid, tested all of it. :-) Doing that as we start using
> > them is going to be good enough.
>
> It is indeed a lot of args. I double checked most of them, so it should
> be mostly correct, but unfortunately I am not 100% sure. I did notice
> discrepancies between the docs and the source code of testpmd too.
> Although not ideal, I am inclining to update the definitions whenever a
> newly implemented test case hits a roadblock.
>
> One thing that I don't remember if I mentioned so far, is the "XYPair".
> You see --flag=X,[Y] in the docs, but I am sure to have read somewhere
> this is potentially just a comma-separated multiple value.
>
> > On Tue, Mar 26, 2024 at 8:04 PM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
> >>
> >> Implement all the testpmd shell parameters into a data structure.
> >>
> >> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> >> Reviewed-by: Jack Bond-Preston <jack.bond-preston@arm.com>
> >> Reviewed-by: Honnappa Nagarahalli <honnappa.nagarahalli@arm.com>
> >> ---
> >>   dts/framework/remote_session/testpmd_shell.py | 633 +++++++++++++++++-
> >>   1 file changed, 615 insertions(+), 18 deletions(-)
> >>
> >> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
> >> index db3abb7600..a823dc53be 100644
> >> --- a/dts/framework/remote_session/testpmd_shell.py
> >> +++ b/dts/framework/remote_session/testpmd_shell.py
> >
> > <snip>
> >
> >> +@str_mixins(bracketed, comma_separated)
> >> +class TestPmdRingNUMAConfig(NamedTuple):
> >> +    """Tuple associating DPDK port, direction of the flow and NUMA socket."""
> >
> > Is there any particular order for these various classes?
>
> No, there is no actual order, potential dependencies aside.
>

Ok, can we order them according to when they appear in the code? Maybe
they already are.

> >> +
> >> +    port: int
> >> +    direction: TestPmdFlowDirection
> >> +    socket: int
> >> +
> >> +
> >
> > <snip>
> >
> >> +@dataclass(kw_only=True)
> >> +class TestPmdTXOnlyForwardingMode(Params):
> >
> > The three special forwarding modes should really be moved right after
> > TestPmdForwardingModes. Do we actually need these three in
> > TestPmdForwardingModes? Looks like we could just remove those from
> > TestPmdForwardingModes since they have to be passed separately, not as
> > that Enum.
>
> Can move and no we don't really need them in TestPmdForwardingModes,
> they can be hardcoded in their own special classes.
>
> >> +    __forward_mode: Literal[TestPmdForwardingModes.txonly] = field(
> >> +        default=TestPmdForwardingModes.txonly, init=False, metadata=long("forward-mode")
> >> +    )
> >
> > I guess this is here so that "--forward-mode=txonly" gets rendered,
> > right? Why the two underscored? Is that because we want to hammer home
> > the fact that this is init=False, a kind of internal field? I'd like
> > to make it like the other fields, without any underscores (or maybe
> > just one underscore), and documented (definitely documented).
> > If we remove txonly from the Enum, we could just have the string value
> > here. The Enums are mostly useful to give users the proper range of
> > values.
> >
>
> Correct and correct. A double underscore would ensure no access to this
> field, which is fixed and only there for rendering purposes... (also the
> developer doesn't get a hint from the IDE, at least not on VS code) and
> in the case of TestPmdForwardingModes it would remove a potential
> conflict. It can definitely be documented though.
>

Ok, can we do a single underscore? I don't really see a reason for two
underscores.

> >> +    multi_flow: Option = field(default=None, metadata=long("txonly-multi-flow"))
> >> +    """Generate multiple flows."""
> >> +    segments_length: XYPair | None = field(default=None, metadata=long("txpkts"))
> >> +    """Set TX segment sizes or total packet length."""
> >> +
> >> +
> >> +@dataclass(kw_only=True)
> >> +class TestPmdFlowGenForwardingMode(Params):
> >> +    __forward_mode: Literal[TestPmdForwardingModes.flowgen] = field(
> >> +        default=TestPmdForwardingModes.flowgen, init=False, metadata=long("forward-mode")
> >> +    )
> >> +    clones: int | None = field(default=None, metadata=long("flowgen-clones"))
> >> +    """Set the number of each packet clones to be sent. Sending clones reduces host CPU load on
> >> +    creating packets and may help in testing extreme speeds or maxing out Tx packet performance.
> >> +    N should be not zero, but less than ‘burst’ parameter.
> >> +    """
> >> +    flows: int | None = field(default=None, metadata=long("flowgen-flows"))
> >> +    """Set the number of flows to be generated, where 1 <= N <= INT32_MAX."""
> >> +    segments_length: XYPair | None = field(default=None, metadata=long("txpkts"))
> >> +    """Set TX segment sizes or total packet length."""
> >> +
> >> +
> >> +@dataclass(kw_only=True)
> >> +class TestPmdNoisyForwardingMode(Params):
> >> +    __forward_mode: Literal[TestPmdForwardingModes.noisy] = field(
> >> +        default=TestPmdForwardingModes.noisy, init=False, metadata=long("forward-mode")
> >> +    )
> >
> > Are both of __forward_mode and forward_mode needed because we need to
> > render both?
>
> Yes, this would render as `--forward-mode=noisy --noisy-forward-mode=io`
> using IO as example.
>
> >> +    forward_mode: (
> >> +        Literal[
> >> +            TestPmdForwardingModes.io,
> >> +            TestPmdForwardingModes.mac,
> >> +            TestPmdForwardingModes.macswap,
> >> +            TestPmdForwardingModes.fivetswap,
> >> +        ]
> >> +        | None
> >
> > Is there a difference between using union (TestPmdForwardingModes.io |
> > TestPmdForwardingModes.mac etc.) and Literal?
>
> TestPmdForwardingModes.io etc are literals and mypy complains:
>
> error: Invalid type: try using Literal[TestPmdForwardingModes.io]
> instead?  [misc]
>
> Therefore they need to be wrapped in Literal[..]
>
> Literal[A, B] is the equivalent of Union[Literal[A], Literal[B]]
>
> So this ultimately renders as Union[Lit[io], Lit[mac], Lit[macswap],
> Lit[fivetswap], None]. So it's really a matter of conciseness, by using
> Literal[A, ..], vs intuitiveness, by using Literal[A] | Literal[..] | ..
>
> Which one would we prefer?
>

Thanks, for the explanation, the way it's now is the most
straightforward, do I'd keep that.

> >> +@dataclass
> >> +class TestPmdDisableRSS(Params):
> >> +    """Disable RSS (Receive Side Scaling)."""
> >
> > Let's put the explanation/reminder of what RSS stands for to either
> > all three classes or none of them.
> >
>
> Ack.
> >> +    rss: TestPmdDisableRSS | TestPmdSetRSSIPOnly | TestPmdSetRSSUDP | None = None
> >> +    """RSS option setting.
> >> +
> >> +    The value can be one of:
> >> +    * :class:`TestPmdDisableRSS`, to disable RSS
> >> +    * :class:`TestPmdSetRSSIPOnly`, to set RSS for IPv4/IPv6 only
> >> +    * :class:`TestPmdSetRSSUDP`, to set RSS for IPv4/IPv6 and UDP
> >> +    """
> >
> > Have you thought about making an Enum where values would be these
> > classes? That could simplify things a bit for users if it works.
>
> It would be lovely to have classes as enum values, and I thought of it
> thinking of other languages like Rust. Not sure this is possible in
> Python. Are you suggesting to pass a class type as a value? In the hope
> that doing:
>
>    TestPmdRSS.Disable()
>
> could work? As this wouldn't. What works instead is:
>
>    TestPmdRSS.Disable.value()
>
> Which is somewhat ugly. Maybe I could modify the behaviour of the enum
> to return the underlying value instead of a reference to the field.
>
> Do you have any better ideas?
>

Not sure if it's better, but I was just thinking:
class RSSEnum(Enum):
    Disable: TestPmdDisableRSS()
    IPOnly: TestPmdSetRSSIPOnly()
    UDP: TestPmdSetRSSIPOnly()

with
rss: RSSEnum | None = None

In this case, the value of the field would be RSSEnum.Disable, but I
don't think that would work, as you mentioned.

Having these three neatly in one object would make it obvious that
these are the rss options, so I think it's worth exploring this a bit
more, but I don't have a solution.

> >> +
> >> +    forward_mode: (
> >> +        Literal[
> >> +            TestPmdForwardingModes.io,
> >> +            TestPmdForwardingModes.mac,
> >> +            TestPmdForwardingModes.macswap,
> >> +            TestPmdForwardingModes.rxonly,
> >> +            TestPmdForwardingModes.csum,
> >> +            TestPmdForwardingModes.icmpecho,
> >> +            TestPmdForwardingModes.ieee1588,
> >> +            TestPmdForwardingModes.fivetswap,
> >> +            TestPmdForwardingModes.shared_rxq,
> >> +            TestPmdForwardingModes.recycle_mbufs,
> >> +        ]
> >
> > This could result in just TestPmdForwardingModes | the rest if we
> > remove the compound fw modes from TestPmdForwardingModes. Maybe we
> > could rename TestPmdForwardingModes to TestPmdSimpleForwardingModes or
> > something at that point.
>
> Yes, good idea.
>
> >> +        | TestPmdFlowGenForwardingMode
> >> +        | TestPmdTXOnlyForwardingMode
> >> +        | TestPmdNoisyForwardingMode
> >> +        | None
> >> +    ) = TestPmdForwardingModes.io
> >> +    """Set the forwarding mode.
> >
> > <snip>
> >
> >> +    mempool_allocation_mode: (
> >> +        Literal[
> >> +            TestPmdMempoolAllocationMode.native,
> >> +            TestPmdMempoolAllocationMode.xmem,
> >> +            TestPmdMempoolAllocationMode.xmemhuge,
> >> +        ]
> >> +        | TestPmdAnonMempoolAllocationMode
> >> +        | None
> >
> > This looks similar to fw modes, maybe the same applies here as well.
>
> Ack.
>
> >> +    ) = field(default=None, metadata=long("mp-alloc"))
> >> +    """Select mempool allocation mode.
> >> +
> >> +    The value can be one of:
> >> +    * :attr:`TestPmdMempoolAllocationMode.native`
> >> +    * :class:`TestPmdAnonMempoolAllocationMode`
> >> +    * :attr:`TestPmdMempoolAllocationMode.xmem`
> >> +    * :attr:`TestPmdMempoolAllocationMode.xmemhuge`
> >> +    """
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 4/6] dts: use testpmd params for scatter test suite
  2024-04-10 10:53     ` Luca Vizzarro
@ 2024-04-10 13:18       ` Juraj Linkeš
  2024-04-26 18:06         ` Jeremy Spewock
  0 siblings, 1 reply; 159+ messages in thread
From: Juraj Linkeš @ 2024-04-10 13:18 UTC (permalink / raw)
  To: Luca Vizzarro
  Cc: Jeremy Spewock, dev, Jack Bond-Preston, Honnappa Nagarahalli

On Wed, Apr 10, 2024 at 12:53 PM Luca Vizzarro <Luca.Vizzarro@arm.com> wrote:
>
> On 09/04/2024 20:12, Juraj Linkeš wrote:
> >> @@ -104,16 +108,15 @@ def pmd_scatter(self, mbsize: int) -> None:
> >>           """
> >>           testpmd = self.sut_node.create_interactive_shell(
> >>               TestPmdShell,
> >> -            app_parameters=StrParams(
> >> -                "--mbcache=200 "
> >> -                f"--mbuf-size={mbsize} "
> >> -                "--max-pkt-len=9000 "
> >> -                "--port-topology=paired "
> >> -                "--tx-offloads=0x00008000"
> >> +            app_parameters=TestPmdParameters(
> >> +                forward_mode=TestPmdForwardingModes.mac,
> >> +                mbcache=200,
> >> +                mbuf_size=[mbsize],
> >> +                max_pkt_len=9000,
> >> +                tx_offloads=0x00008000,
> >>               ),
> >>               privileged=True,
> >>           )
> >> -        testpmd.set_forward_mode(TestPmdForwardingModes.mac)
> >
> > Jeremy, does this change the test? Instead of configuring the fw mode
> > after starting testpmd, we're starting testpmd with fw mode
> > configured.
>
> I am not Jeremy (please Jeremy still reply), but we discussed this on
> Slack. Reading through the testpmd source code, setting arguments like
> forward-mode in the command line, is the exact equivalent of calling
> `set forward mode` right after start-up. So it is equivalent in theory.
>
> > If not, we should remove the testpmd.set_forward_mode method, as it's
> > not used anymore.
>
> Could there be test cases that change the forward mode multiple times in
> the same shell, though? As this could still be needed to cover this.

Yes, but we don't have such a test now. It's good practice to remove
unused code. We can still bring it back anytime, it'll be in git
history.

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 5/6] dts: add statefulness to InteractiveShell
  2024-04-10 11:27       ` Luca Vizzarro
@ 2024-04-10 13:35         ` Juraj Linkeš
  2024-04-10 14:07           ` Luca Vizzarro
  2024-04-29 14:48           ` Jeremy Spewock
  0 siblings, 2 replies; 159+ messages in thread
From: Juraj Linkeš @ 2024-04-10 13:35 UTC (permalink / raw)
  To: Luca Vizzarro
  Cc: Jeremy Spewock, dev, Jack Bond-Preston, Honnappa Nagarahalli

On Wed, Apr 10, 2024 at 1:27 PM Luca Vizzarro <Luca.Vizzarro@arm.com> wrote:
>
> On 10/04/2024 07:53, Juraj Linkeš wrote:
> > I have a general question. What are these changes for? Do you
> > anticipate us needing this in the future? Wouldn't it be better to add
> > it only when we need it?
>
> It's been sometime since we raised this task internally. This patch and
> the next one arise from some survey done on old DTS test cases.
> Unfortunately, I can't pinpoint.
>
> Specifically for this patch though, the timeout bit is useful in
> conjunction with the related change in the next. Instead of giving an
> optional timeout argument to all the commands where we may want to
> change it, aren't we better off with providing a facility to temporarily
> change this for the current scope?
>

This is a good question. If the scope is just one command, then no. If
it's more than one, then maybe yes. I don't know which is better.

We should also consider that this would introduce a difference in API
between the interactive and non-interactive sessions. Do we want to do
this there as well?

Also, maybe set_timeout should be a property or we could just make
_timeout public.
And is_privileged should just be privileged, as it's a property (which
shouldn't contain a verb; if it was a method it would be a good name).

> >
> > On Thu, Mar 28, 2024 at 5:48 PM Jeremy Spewock <jspewock@iol.unh.edu> wrote:
> >>
> >> On Tue, Mar 26, 2024 at 3:04 PM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
> >> <snip>
> >>> diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
> >>> index a2c7b30d9f..5d80061e8d 100644
> >>> --- a/dts/framework/remote_session/interactive_shell.py
> >>> +++ b/dts/framework/remote_session/interactive_shell.py
> >>> @@ -41,8 +41,10 @@ class InteractiveShell(ABC):
> >>>       _stdout: channel.ChannelFile
> >>>       _ssh_channel: Channel
> >>>       _logger: DTSLogger
> >>> +    __default_timeout: float
> >>
> >> Only single underscores are used for other private variables, probably
> >> better to keep that consistent with this one.
> >>
> >
> > I agree, I don't see a reason for the double underscore.
>
> Ack.
>
> >
> >>>       _timeout: float
> >>>       _app_args: Params | None
> >>> +    _is_privileged: bool = False
> >> <snip>
> >>> 2.34.1
> >>>
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 5/6] dts: add statefulness to InteractiveShell
  2024-04-10 13:35         ` Juraj Linkeš
@ 2024-04-10 14:07           ` Luca Vizzarro
  2024-04-12 12:33             ` Juraj Linkeš
  2024-04-29 14:48           ` Jeremy Spewock
  1 sibling, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-04-10 14:07 UTC (permalink / raw)
  To: Juraj Linkeš
  Cc: Jeremy Spewock, dev, Jack Bond-Preston, Honnappa Nagarahalli

On 10/04/2024 14:35, Juraj Linkeš wrote:
> We should also consider that this would introduce a difference in API
> between the interactive and non-interactive sessions. Do we want to do
> this there as well?

Could definitely add it there as well. You are referring to 
RemoteSession I presume, right?

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 6/6] dts: add statefulness to TestPmdShell
  2024-04-10 11:35       ` Luca Vizzarro
@ 2024-04-11 10:30         ` Juraj Linkeš
  2024-04-11 11:47           ` Luca Vizzarro
  0 siblings, 1 reply; 159+ messages in thread
From: Juraj Linkeš @ 2024-04-11 10:30 UTC (permalink / raw)
  To: Luca Vizzarro
  Cc: Jeremy Spewock, dev, Jack Bond-Preston, Honnappa Nagarahalli

I overlooked this reply initially.

On Wed, Apr 10, 2024 at 1:35 PM Luca Vizzarro <Luca.Vizzarro@arm.com> wrote:
>
> On 10/04/2024 08:41, Juraj Linkeš wrote:
> >> <snip>
> >>> @@ -723,7 +731,13 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
> >>>           if self._app_args.app_params is None:
> >>>               self._app_args.app_params = TestPmdParameters()
> >>>
> >>> -        self.number_of_ports = len(self._app_args.ports) if self._app_args.ports is not None else 0
> >>> +        assert isinstance(self._app_args.app_params, TestPmdParameters)
> >>> +
> >>
> >> This is tricky because ideally we wouldn't have the assertion here,
> >> but I understand why it is needed because Eal parameters have app args
> >> which can be any instance of params. I'm not sure of the best way to
> >> solve this, because making testpmd parameters extend from eal would
> >> break the general scheme that you have in place, and having an
> >> extension of EalParameters that enforces this app_args is
> >> TestPmdParameters would solve the issues, but might be a little
> >> clunky. Is there a way we can use a generic to get python to just
> >> understand that, in this case, this will always be TestPmdParameters?
> >> If not I might prefer making a private class where this is
> >> TestPmdParameters, just because there aren't really any other
> >> assertions that we use elsewhere and an unexpected exception from this
> >> (even though I don't think that can happen) could cause people some
> >> issues.
> >>
> >> It might be the case that an assertion is the easiest way to deal with
> >> it though, what do you think?
> >>
> >
> > We could change the signature (just the type of app_args) of the init
> > method - I think we should be able to create a type that's
> > EalParameters with .app_params being TestPmdParameters or None. The
> > init method would just call super().
> >
> > Something like the above is basically necessary with inheritance where
> > subclasses are all extensions (not just implementations) of the
> > superclass (having differences in API).
> >
>
> I believe this is indeed a tricky one. But, unfortunately, I am not
> understanding the solution that is being proposed. To me, it just feels
> like using a generic factory like:
>
>    self.sut_node.create_interactive_shell(..)
>
> is one of the reasons to bring in the majority of these complexities.
>

I've been thinking about these interactive shell constructors for some
time and I think the factory pattern is not well suitable for this.
Factories work well with classes with the same API (i.e.
implementations of abstract classes that don't add anything extra),
but are much less useful when dealing with classes with different
behaviors, such as the interactive shells. We see this here, different
apps are going to require different args and that alone kinda breaks
the factory pattern. I think we'll need to either ditch these
factories and instead just have methods that return the proper shell
(and the methods would only exist in classes where they belong, e.g.
testpmd only makes sense on an SUT). Or we could overload each factory
(the support has only been added in 3.11 with @typing.overload, but is
also available in typing_extensions, so we would be able to use it
with the extra dependency) where different signatures would return
different objects. In both cases the caller won't have to import the
class and the method signature is going to be clearer.

We have this pattern with sut/tg nodes. I decided to move away from
the node factory because it didn't add much and in fact the code was
only clunkier. The interactive shell is not quite the same, as the
shells are not standalone in the same way the nodes are (the shells
are tied to nodes). Let me know what you think about all this - both
Luca and Jeremy.

> What do you mean by creating this new type that combines EalParams and
> TestPmdParams?

Let me illustrate this on the TestPmdShell __init__() method I had in mind:

def __init__(self, interactive_session: SSHClient,
        logger: DTSLogger,
        get_privileged_command: Callable[[str], str] | None,
        app_args: EalTestPmdParams | None = None,
        timeout: float = SETTINGS.timeout,
    ) -> None:
    super().__init__(interactive_session, logger, get_privileged_command)
    self.state = TestPmdState()

Where EalTestPmdParams would be something that enforces that
app_args.app_params is of the TestPmdParameters type.

But thinking more about this, we're probably better off switching the
params composition. Instead of TestPmdParameters being part of
EalParameters, we do it the other way around. This way the type of
app_args could just be TestPmdParameters and the types should work.
Or we pass the args separately, but that would likely require ditching
the factories and replacing them with methods (or overloading them).

And hopefully the imports won't be impossible to solve. :-)

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 6/6] dts: add statefulness to TestPmdShell
  2024-04-11 10:30         ` Juraj Linkeš
@ 2024-04-11 11:47           ` Luca Vizzarro
  2024-04-11 12:13             ` Juraj Linkeš
  0 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-04-11 11:47 UTC (permalink / raw)
  To: Juraj Linkeš
  Cc: Jeremy Spewock, dev, Jack Bond-Preston, Honnappa Nagarahalli

On 11/04/2024 11:30, Juraj Linkeš wrote:
> I've been thinking about these interactive shell constructors for some
> time and I think the factory pattern is not well suitable for this.
> Factories work well with classes with the same API (i.e.
> implementations of abstract classes that don't add anything extra),
> but are much less useful when dealing with classes with different
> behaviors, such as the interactive shells. We see this here, different
> apps are going to require different args and that alone kinda breaks
> the factory pattern. I think we'll need to either ditch these
> factories and instead just have methods that return the proper shell
> (and the methods would only exist in classes where they belong, e.g.
> testpmd only makes sense on an SUT). Or we could overload each factory
> (the support has only been added in 3.11 with @typing.overload, but is
> also available in typing_extensions, so we would be able to use it
> with the extra dependency) where different signatures would return
> different objects. In both cases the caller won't have to import the
> class and the method signature is going to be clearer.
> 
> We have this pattern with sut/tg nodes. I decided to move away from
> the node factory because it didn't add much and in fact the code was
> only clunkier. The interactive shell is not quite the same, as the
> shells are not standalone in the same way the nodes are (the shells
> are tied to nodes). Let me know what you think about all this - both
> Luca and Jeremy.

When writing this series, I went down the path of creating a 
`create_testpmd_shell` method at some point as a solution to these 
problems. Realising after that it may be too big of a change, and 
possibly best left to a discussion exactly like this one.

Generics used at this level may be a bit too much, especially for 
Python, as support is not *that* great. I am of the opinion that having 
a dedicated wrapper is easier for the developer and the user. Generics 
are not needed to this level anyways, as we have a limited selection of 
shells that are actually going to be used.

We can also swap the wrapping process to simplify things, instead of:

   shell = self.sut_node.create_interactive_shell(TestPmdShell, ..)

do:

   shell = TestPmdShell(self.sut_node, ..)

Let the Shell class ingest the node, and not the other way round.

The current approach appears to me to be top-down instead of bottom-up. 
We take the most abstracted part and we work our way down. But all we 
want is concreteness to the end user (developer).

> Let me illustrate this on the TestPmdShell __init__() method I had in mind:
> 
> def __init__(self, interactive_session: SSHClient,
>          logger: DTSLogger,
>          get_privileged_command: Callable[[str], str] | None,
>          app_args: EalTestPmdParams | None = None,
>          timeout: float = SETTINGS.timeout,
>      ) -> None:
>      super().__init__(interactive_session, logger, get_privileged_command)
>      self.state = TestPmdState()
> 
> Where EalTestPmdParams would be something that enforces that
> app_args.app_params is of the TestPmdParameters type.
> 
> But thinking more about this, we're probably better off switching the
> params composition. Instead of TestPmdParameters being part of
> EalParameters, we do it the other way around. This way the type of
> app_args could just be TestPmdParameters and the types should work.
> Or we pass the args separately, but that would likely require ditching
> the factories and replacing them with methods (or overloading them).
> 
> And hopefully the imports won't be impossible to solve. :-)

It is what I feared, and I think it may become even more convoluted. As 
you said, ditching the factories will simplify things and make it more 
straightforward. So, we wouldn't find ourselves in problems like these.

I don't have a strong preference in approach between:
* overloading node methods
* dedicated node methods
* let the shells ingest nodes instead

But if I were to give priority, I'd take it from last to first. Letting 
shells ingest nodes will decouple the situation adding an extra step of 
simplification. I may not see the full picture though. The two are 
reasonable but, having a dedicated node method will stop the requirement 
to import the shell we need, and it's pretty much equivalent... but 
overloading also is very new to Python, so I may prefer to stick to more 
established.

Letting TestPmdParams take EalParams, instead of the other way around, 
would naturally follow the bottom-up approach too. Allowing Params to 
arbitrarily append string arguments – as proposed, would also allow 
users to use a plain (EalParams + string). So sounds like a good 
approach overall.

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 6/6] dts: add statefulness to TestPmdShell
  2024-04-11 11:47           ` Luca Vizzarro
@ 2024-04-11 12:13             ` Juraj Linkeš
  2024-04-11 13:59               ` Luca Vizzarro
  2024-04-26 18:06               ` Jeremy Spewock
  0 siblings, 2 replies; 159+ messages in thread
From: Juraj Linkeš @ 2024-04-11 12:13 UTC (permalink / raw)
  To: Luca Vizzarro
  Cc: Jeremy Spewock, dev, Jack Bond-Preston, Honnappa Nagarahalli

On Thu, Apr 11, 2024 at 1:47 PM Luca Vizzarro <Luca.Vizzarro@arm.com> wrote:
>
> On 11/04/2024 11:30, Juraj Linkeš wrote:
> > I've been thinking about these interactive shell constructors for some
> > time and I think the factory pattern is not well suitable for this.
> > Factories work well with classes with the same API (i.e.
> > implementations of abstract classes that don't add anything extra),
> > but are much less useful when dealing with classes with different
> > behaviors, such as the interactive shells. We see this here, different
> > apps are going to require different args and that alone kinda breaks
> > the factory pattern. I think we'll need to either ditch these
> > factories and instead just have methods that return the proper shell
> > (and the methods would only exist in classes where they belong, e.g.
> > testpmd only makes sense on an SUT). Or we could overload each factory
> > (the support has only been added in 3.11 with @typing.overload, but is
> > also available in typing_extensions, so we would be able to use it
> > with the extra dependency) where different signatures would return
> > different objects. In both cases the caller won't have to import the
> > class and the method signature is going to be clearer.
> >
> > We have this pattern with sut/tg nodes. I decided to move away from
> > the node factory because it didn't add much and in fact the code was
> > only clunkier. The interactive shell is not quite the same, as the
> > shells are not standalone in the same way the nodes are (the shells
> > are tied to nodes). Let me know what you think about all this - both
> > Luca and Jeremy.
>
> When writing this series, I went down the path of creating a
> `create_testpmd_shell` method at some point as a solution to these
> problems. Realising after that it may be too big of a change, and
> possibly best left to a discussion exactly like this one.
>

The changes we discuss below don't seem that big. What do you think,
do we just add another patch to the series?

> Generics used at this level may be a bit too much, especially for
> Python, as support is not *that* great. I am of the opinion that having
> a dedicated wrapper is easier for the developer and the user. Generics
> are not needed to this level anyways, as we have a limited selection of
> shells that are actually going to be used.
>
> We can also swap the wrapping process to simplify things, instead of:
>
>    shell = self.sut_node.create_interactive_shell(TestPmdShell, ..)
>
> do:
>
>    shell = TestPmdShell(self.sut_node, ..)
>
> Let the Shell class ingest the node, and not the other way round.
>

I thought about this a bit as well, it's a good approach. The current
design is top-down, as you say, in that "I have a node and I do things
with the node, including starting testpmd on the node". But it could
also be "I have a node, but I also have other non-node resources at my
disposal and it's up to me how I utilize those". If we can make the
imports work then this is likely the best option.

> The current approach appears to me to be top-down instead of bottom-up.
> We take the most abstracted part and we work our way down. But all we
> want is concreteness to the end user (developer).
>
> > Let me illustrate this on the TestPmdShell __init__() method I had in mind:
> >
> > def __init__(self, interactive_session: SSHClient,
> >          logger: DTSLogger,
> >          get_privileged_command: Callable[[str], str] | None,
> >          app_args: EalTestPmdParams | None = None,
> >          timeout: float = SETTINGS.timeout,
> >      ) -> None:
> >      super().__init__(interactive_session, logger, get_privileged_command)
> >      self.state = TestPmdState()
> >
> > Where EalTestPmdParams would be something that enforces that
> > app_args.app_params is of the TestPmdParameters type.
> >
> > But thinking more about this, we're probably better off switching the
> > params composition. Instead of TestPmdParameters being part of
> > EalParameters, we do it the other way around. This way the type of
> > app_args could just be TestPmdParameters and the types should work.
> > Or we pass the args separately, but that would likely require ditching
> > the factories and replacing them with methods (or overloading them).
> >
> > And hopefully the imports won't be impossible to solve. :-)
>
> It is what I feared, and I think it may become even more convoluted. As
> you said, ditching the factories will simplify things and make it more
> straightforward. So, we wouldn't find ourselves in problems like these.
>
> I don't have a strong preference in approach between:
> * overloading node methods
> * dedicated node methods
> * let the shells ingest nodes instead
>
> But if I were to give priority, I'd take it from last to first. Letting
> shells ingest nodes will decouple the situation adding an extra step of
> simplification.

+1 for simplification.

> I may not see the full picture though. The two are
> reasonable but, having a dedicated node method will stop the requirement
> to import the shell we need, and it's pretty much equivalent... but
> overloading also is very new to Python, so I may prefer to stick to more
> established.
>

Let's try shells ingesting nodes if the imports work out then. If not,
we can fall back to dedicated node methods.

> Letting TestPmdParams take EalParams, instead of the other way around,
> would naturally follow the bottom-up approach too. Allowing Params to
> arbitrarily append string arguments – as proposed, would also allow
> users to use a plain (EalParams + string). So sounds like a good
> approach overall.

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 6/6] dts: add statefulness to TestPmdShell
  2024-04-11 12:13             ` Juraj Linkeš
@ 2024-04-11 13:59               ` Luca Vizzarro
  2024-04-26 18:06               ` Jeremy Spewock
  1 sibling, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-04-11 13:59 UTC (permalink / raw)
  To: Juraj Linkeš
  Cc: Jeremy Spewock, dev, Jack Bond-Preston, Honnappa Nagarahalli


On 11/04/2024 13:13, Juraj Linkeš wrote:
> The changes we discuss below don't seem that big. What do you think,
> do we just add another patch to the series?

Sure thing, I can take this and add it to v2.

> I thought about this a bit as well, it's a good approach. The current
> design is top-down, as you say, in that "I have a node and I do things
> with the node, including starting testpmd on the node". But it could
> also be "I have a node, but I also have other non-node resources at my
> disposal and it's up to me how I utilize those". If we can make the
> imports work then this is likely the best option.
> 
> <snip>
> 
> +1 for simplification.
>
> <snip> >
> Let's try shells ingesting nodes if the imports work out then. If not,
> we can fall back to dedicated node methods.

Sounds good!


^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 5/6] dts: add statefulness to InteractiveShell
  2024-04-10 14:07           ` Luca Vizzarro
@ 2024-04-12 12:33             ` Juraj Linkeš
  0 siblings, 0 replies; 159+ messages in thread
From: Juraj Linkeš @ 2024-04-12 12:33 UTC (permalink / raw)
  To: Luca Vizzarro
  Cc: Jeremy Spewock, dev, Jack Bond-Preston, Honnappa Nagarahalli

On Wed, Apr 10, 2024 at 4:07 PM Luca Vizzarro <Luca.Vizzarro@arm.com> wrote:
>
> On 10/04/2024 14:35, Juraj Linkeš wrote:
> > We should also consider that this would introduce a difference in API
> > between the interactive and non-interactive sessions. Do we want to do
> > this there as well?
>
> Could definitely add it there as well. You are referring to
> RemoteSession I presume, right?

Yes.

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 4/6] dts: use testpmd params for scatter test suite
  2024-04-10 13:18       ` Juraj Linkeš
@ 2024-04-26 18:06         ` Jeremy Spewock
  2024-04-29  7:45           ` Juraj Linkeš
  0 siblings, 1 reply; 159+ messages in thread
From: Jeremy Spewock @ 2024-04-26 18:06 UTC (permalink / raw)
  To: Juraj Linkeš
  Cc: Luca Vizzarro, dev, Jack Bond-Preston, Honnappa Nagarahalli

On Wed, Apr 10, 2024 at 9:19 AM Juraj Linkeš <juraj.linkes@pantheon.tech> wrote:
>
> On Wed, Apr 10, 2024 at 12:53 PM Luca Vizzarro <Luca.Vizzarro@arm.com> wrote:
> >
> > On 09/04/2024 20:12, Juraj Linkeš wrote:
> > >> @@ -104,16 +108,15 @@ def pmd_scatter(self, mbsize: int) -> None:
> > >>           """
> > >>           testpmd = self.sut_node.create_interactive_shell(
> > >>               TestPmdShell,
> > >> -            app_parameters=StrParams(
> > >> -                "--mbcache=200 "
> > >> -                f"--mbuf-size={mbsize} "
> > >> -                "--max-pkt-len=9000 "
> > >> -                "--port-topology=paired "
> > >> -                "--tx-offloads=0x00008000"
> > >> +            app_parameters=TestPmdParameters(
> > >> +                forward_mode=TestPmdForwardingModes.mac,
> > >> +                mbcache=200,
> > >> +                mbuf_size=[mbsize],
> > >> +                max_pkt_len=9000,
> > >> +                tx_offloads=0x00008000,
> > >>               ),
> > >>               privileged=True,
> > >>           )
> > >> -        testpmd.set_forward_mode(TestPmdForwardingModes.mac)
> > >
> > > Jeremy, does this change the test? Instead of configuring the fw mode
> > > after starting testpmd, we're starting testpmd with fw mode
> > > configured.

To my knowledge, as Luca mentions below, this does not functionally
change anything about the test, scatter should just need the MAC
forwarding mode to be set at some point before forwarding starts, it
doesn't technically matter when. One thing to note that this does
change, however, is that we lose the verification step that the method
within testpmd provides. I'm not sure off the top of my head if
testpmd just completely fails to start if the forwarding mode flag is
set and it fails to change modes or if it still starts and then just
goes back to default (io) which would make the test operate in an
invalid state without anyway of knowing.

As another note however, I've never seen a mode change fail and I
don't know what could even make it fail, so this would be a rare thing
anyway, but still just something to consider.


> >
> > I am not Jeremy (please Jeremy still reply), but we discussed this on
> > Slack. Reading through the testpmd source code, setting arguments like
> > forward-mode in the command line, is the exact equivalent of calling
> > `set forward mode` right after start-up. So it is equivalent in theory.
> >
> > > If not, we should remove the testpmd.set_forward_mode method, as it's
> > > not used anymore.
> >
> > Could there be test cases that change the forward mode multiple times in
> > the same shell, though? As this could still be needed to cover this.
>
> Yes, but we don't have such a test now. It's good practice to remove
> unused code. We can still bring it back anytime, it'll be in git
> history.

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 6/6] dts: add statefulness to TestPmdShell
  2024-04-11 12:13             ` Juraj Linkeš
  2024-04-11 13:59               ` Luca Vizzarro
@ 2024-04-26 18:06               ` Jeremy Spewock
  2024-04-29 12:06                 ` Juraj Linkeš
  1 sibling, 1 reply; 159+ messages in thread
From: Jeremy Spewock @ 2024-04-26 18:06 UTC (permalink / raw)
  To: Juraj Linkeš
  Cc: Luca Vizzarro, dev, Jack Bond-Preston, Honnappa Nagarahalli

Apologies for being so late on the discussion, but just a few of my
thoughts:

* I think using something like overloading even though it is new to
python is completely fine because this new python version is a
dependency of  the DTS runner. The DTS runner can have bleeding-edge
requirements because we manage that through a container to make things
easier.

On Thu, Apr 11, 2024 at 8:13 AM Juraj Linkeš <juraj.linkes@pantheon.tech> wrote:
>
> On Thu, Apr 11, 2024 at 1:47 PM Luca Vizzarro <Luca.Vizzarro@arm.com> wrote:
> >
> > On 11/04/2024 11:30, Juraj Linkeš wrote:
> > > I've been thinking about these interactive shell constructors for some
> > > time and I think the factory pattern is not well suitable for this.
> > > Factories work well with classes with the same API (i.e.
> > > implementations of abstract classes that don't add anything extra),
> > > but are much less useful when dealing with classes with different
> > > behaviors, such as the interactive shells. We see this here, different
> > > apps are going to require different args and that alone kinda breaks
> > > the factory pattern. I think we'll need to either ditch these
> > > factories and instead just have methods that return the proper shell
> > > (and the methods would only exist in classes where they belong, e.g.
> > > testpmd only makes sense on an SUT). Or we could overload each factory
> > > (the support has only been added in 3.11 with @typing.overload, but is
> > > also available in typing_extensions, so we would be able to use it
> > > with the extra dependency) where different signatures would return
> > > different objects. In both cases the caller won't have to import the
> > > class and the method signature is going to be clearer.
> > >
> > > We have this pattern with sut/tg nodes. I decided to move away from
> > > the node factory because it didn't add much and in fact the code was
> > > only clunkier. The interactive shell is not quite the same, as the
> > > shells are not standalone in the same way the nodes are (the shells
> > > are tied to nodes). Let me know what you think about all this - both
> > > Luca and Jeremy.
> >
> > When writing this series, I went down the path of creating a
> > `create_testpmd_shell` method at some point as a solution to these
> > problems. Realising after that it may be too big of a change, and
> > possibly best left to a discussion exactly like this one.
> >
>
> The changes we discuss below don't seem that big. What do you think,
> do we just add another patch to the series?
>
> > Generics used at this level may be a bit too much, especially for
> > Python, as support is not *that* great. I am of the opinion that having
> > a dedicated wrapper is easier for the developer and the user. Generics
> > are not needed to this level anyways, as we have a limited selection of
> > shells that are actually going to be used.
> >
> > We can also swap the wrapping process to simplify things, instead of:
> >
> >    shell = self.sut_node.create_interactive_shell(TestPmdShell, ..)
> >
> > do:
> >
> >    shell = TestPmdShell(self.sut_node, ..)
> >
> > Let the Shell class ingest the node, and not the other way round.
> >
>
> I thought about this a bit as well, it's a good approach. The current
> design is top-down, as you say, in that "I have a node and I do things
> with the node, including starting testpmd on the node". But it could
> also be "I have a node, but I also have other non-node resources at my
> disposal and it's up to me how I utilize those". If we can make the
> imports work then this is likely the best option.

It might be me slightly stuck in the old ways of doing things, but I
might slightly favor the overloading methods approach. This is really
because, at least in my mind, the SUT node is somewhat of a central
API for the developer to use during testing, so having a method on
that API for creating a shell for you to use on the node makes sense
to me. It creates more of a "one stop shop" kind of idea where
developers have to do less reading about how to do things and can just
look at the methods of the SUT node to get what they would need.

That being said, I think in any other framework the passing of the
node into the shell would easily make more sense and I'm not opposed
to going that route either. In general, I agree that not using a
factory with a generic will make things much easier in the future.

>
> > The current approach appears to me to be top-down instead of bottom-up.
> > We take the most abstracted part and we work our way down. But all we
> > want is concreteness to the end user (developer).
> >
> > > Let me illustrate this on the TestPmdShell __init__() method I had in mind:
> > >
> > > def __init__(self, interactive_session: SSHClient,
> > >          logger: DTSLogger,
> > >          get_privileged_command: Callable[[str], str] | None,
> > >          app_args: EalTestPmdParams | None = None,
> > >          timeout: float = SETTINGS.timeout,
> > >      ) -> None:
> > >      super().__init__(interactive_session, logger, get_privileged_command)
> > >      self.state = TestPmdState()
> > >
> > > Where EalTestPmdParams would be something that enforces that
> > > app_args.app_params is of the TestPmdParameters type.
> > >
> > > But thinking more about this, we're probably better off switching the
> > > params composition. Instead of TestPmdParameters being part of
> > > EalParameters, we do it the other way around. This way the type of
> > > app_args could just be TestPmdParameters and the types should work.
> > > Or we pass the args separately, but that would likely require ditching
> > > the factories and replacing them with methods (or overloading them).
> > >
> > > And hopefully the imports won't be impossible to solve. :-)
> >
> > It is what I feared, and I think it may become even more convoluted. As
> > you said, ditching the factories will simplify things and make it more
> > straightforward. So, we wouldn't find ourselves in problems like these.
> >
> > I don't have a strong preference in approach between:
> > * overloading node methods
> > * dedicated node methods
> > * let the shells ingest nodes instead
> >
> > But if I were to give priority, I'd take it from last to first. Letting
> > shells ingest nodes will decouple the situation adding an extra step of
> > simplification.
>
> +1 for simplification.
>
> > I may not see the full picture though. The two are
> > reasonable but, having a dedicated node method will stop the requirement
> > to import the shell we need, and it's pretty much equivalent... but
> > overloading also is very new to Python, so I may prefer to stick to more
> > established.
> >
>
> Let's try shells ingesting nodes if the imports work out then. If not,
> we can fall back to dedicated node methods.
>
> > Letting TestPmdParams take EalParams, instead of the other way around,
> > would naturally follow the bottom-up approach too. Allowing Params to
> > arbitrarily append string arguments – as proposed, would also allow
> > users to use a plain (EalParams + string). So sounds like a good
> > approach overall.

This I like a lot. We don't want to force EalParams to have
TestpmdParams nested inside of them because other DPDK apps might need
them too and this fixes the issue of always having to assert what type
of inner params you have.

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 4/6] dts: use testpmd params for scatter test suite
  2024-04-26 18:06         ` Jeremy Spewock
@ 2024-04-29  7:45           ` Juraj Linkeš
  0 siblings, 0 replies; 159+ messages in thread
From: Juraj Linkeš @ 2024-04-29  7:45 UTC (permalink / raw)
  To: Jeremy Spewock
  Cc: Luca Vizzarro, dev, Jack Bond-Preston, Honnappa Nagarahalli

On Fri, Apr 26, 2024 at 8:06 PM Jeremy Spewock <jspewock@iol.unh.edu> wrote:
>
> On Wed, Apr 10, 2024 at 9:19 AM Juraj Linkeš <juraj.linkes@pantheon.tech> wrote:
> >
> > On Wed, Apr 10, 2024 at 12:53 PM Luca Vizzarro <Luca.Vizzarro@arm.com> wrote:
> > >
> > > On 09/04/2024 20:12, Juraj Linkeš wrote:
> > > >> @@ -104,16 +108,15 @@ def pmd_scatter(self, mbsize: int) -> None:
> > > >>           """
> > > >>           testpmd = self.sut_node.create_interactive_shell(
> > > >>               TestPmdShell,
> > > >> -            app_parameters=StrParams(
> > > >> -                "--mbcache=200 "
> > > >> -                f"--mbuf-size={mbsize} "
> > > >> -                "--max-pkt-len=9000 "
> > > >> -                "--port-topology=paired "
> > > >> -                "--tx-offloads=0x00008000"
> > > >> +            app_parameters=TestPmdParameters(
> > > >> +                forward_mode=TestPmdForwardingModes.mac,
> > > >> +                mbcache=200,
> > > >> +                mbuf_size=[mbsize],
> > > >> +                max_pkt_len=9000,
> > > >> +                tx_offloads=0x00008000,
> > > >>               ),
> > > >>               privileged=True,
> > > >>           )
> > > >> -        testpmd.set_forward_mode(TestPmdForwardingModes.mac)
> > > >
> > > > Jeremy, does this change the test? Instead of configuring the fw mode
> > > > after starting testpmd, we're starting testpmd with fw mode
> > > > configured.
>
> To my knowledge, as Luca mentions below, this does not functionally
> change anything about the test, scatter should just need the MAC
> forwarding mode to be set at some point before forwarding starts, it
> doesn't technically matter when. One thing to note that this does
> change, however, is that we lose the verification step that the method
> within testpmd provides. I'm not sure off the top of my head if
> testpmd just completely fails to start if the forwarding mode flag is
> set and it fails to change modes or if it still starts and then just
> goes back to default (io) which would make the test operate in an
> invalid state without anyway of knowing.
>
> As another note however, I've never seen a mode change fail and I
> don't know what could even make it fail, so this would be a rare thing
> anyway, but still just something to consider.
>

Ok, thanks. This is fine then. If we see a problem with this when
testpmd starts we can just raise a bug against testpmd (as it should
either start with mac forwarding or error).

>
> > >
> > > I am not Jeremy (please Jeremy still reply), but we discussed this on
> > > Slack. Reading through the testpmd source code, setting arguments like
> > > forward-mode in the command line, is the exact equivalent of calling
> > > `set forward mode` right after start-up. So it is equivalent in theory.
> > >
> > > > If not, we should remove the testpmd.set_forward_mode method, as it's
> > > > not used anymore.
> > >
> > > Could there be test cases that change the forward mode multiple times in
> > > the same shell, though? As this could still be needed to cover this.
> >
> > Yes, but we don't have such a test now. It's good practice to remove
> > unused code. We can still bring it back anytime, it'll be in git
> > history.

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 6/6] dts: add statefulness to TestPmdShell
  2024-04-26 18:06               ` Jeremy Spewock
@ 2024-04-29 12:06                 ` Juraj Linkeš
  0 siblings, 0 replies; 159+ messages in thread
From: Juraj Linkeš @ 2024-04-29 12:06 UTC (permalink / raw)
  To: Jeremy Spewock
  Cc: Luca Vizzarro, dev, Jack Bond-Preston, Honnappa Nagarahalli

On Fri, Apr 26, 2024 at 8:06 PM Jeremy Spewock <jspewock@iol.unh.edu> wrote:
>
> Apologies for being so late on the discussion, but just a few of my
> thoughts:
>
> * I think using something like overloading even though it is new to
> python is completely fine because this new python version is a
> dependency of  the DTS runner. The DTS runner can have bleeding-edge
> requirements because we manage that through a container to make things
> easier.
>
> On Thu, Apr 11, 2024 at 8:13 AM Juraj Linkeš <juraj.linkes@pantheon.tech> wrote:
> >
> > On Thu, Apr 11, 2024 at 1:47 PM Luca Vizzarro <Luca.Vizzarro@arm.com> wrote:
> > >
> > > On 11/04/2024 11:30, Juraj Linkeš wrote:
> > > > I've been thinking about these interactive shell constructors for some
> > > > time and I think the factory pattern is not well suitable for this.
> > > > Factories work well with classes with the same API (i.e.
> > > > implementations of abstract classes that don't add anything extra),
> > > > but are much less useful when dealing with classes with different
> > > > behaviors, such as the interactive shells. We see this here, different
> > > > apps are going to require different args and that alone kinda breaks
> > > > the factory pattern. I think we'll need to either ditch these
> > > > factories and instead just have methods that return the proper shell
> > > > (and the methods would only exist in classes where they belong, e.g.
> > > > testpmd only makes sense on an SUT). Or we could overload each factory
> > > > (the support has only been added in 3.11 with @typing.overload, but is
> > > > also available in typing_extensions, so we would be able to use it
> > > > with the extra dependency) where different signatures would return
> > > > different objects. In both cases the caller won't have to import the
> > > > class and the method signature is going to be clearer.
> > > >
> > > > We have this pattern with sut/tg nodes. I decided to move away from
> > > > the node factory because it didn't add much and in fact the code was
> > > > only clunkier. The interactive shell is not quite the same, as the
> > > > shells are not standalone in the same way the nodes are (the shells
> > > > are tied to nodes). Let me know what you think about all this - both
> > > > Luca and Jeremy.
> > >
> > > When writing this series, I went down the path of creating a
> > > `create_testpmd_shell` method at some point as a solution to these
> > > problems. Realising after that it may be too big of a change, and
> > > possibly best left to a discussion exactly like this one.
> > >
> >
> > The changes we discuss below don't seem that big. What do you think,
> > do we just add another patch to the series?
> >
> > > Generics used at this level may be a bit too much, especially for
> > > Python, as support is not *that* great. I am of the opinion that having
> > > a dedicated wrapper is easier for the developer and the user. Generics
> > > are not needed to this level anyways, as we have a limited selection of
> > > shells that are actually going to be used.
> > >
> > > We can also swap the wrapping process to simplify things, instead of:
> > >
> > >    shell = self.sut_node.create_interactive_shell(TestPmdShell, ..)
> > >
> > > do:
> > >
> > >    shell = TestPmdShell(self.sut_node, ..)
> > >
> > > Let the Shell class ingest the node, and not the other way round.
> > >
> >
> > I thought about this a bit as well, it's a good approach. The current
> > design is top-down, as you say, in that "I have a node and I do things
> > with the node, including starting testpmd on the node". But it could
> > also be "I have a node, but I also have other non-node resources at my
> > disposal and it's up to me how I utilize those". If we can make the
> > imports work then this is likely the best option.
>
> It might be me slightly stuck in the old ways of doing things, but I
> might slightly favor the overloading methods approach. This is really
> because, at least in my mind, the SUT node is somewhat of a central
> API for the developer to use during testing, so having a method on
> that API for creating a shell for you to use on the node makes sense
> to me. It creates more of a "one stop shop" kind of idea where
> developers have to do less reading about how to do things and can just
> look at the methods of the SUT node to get what they would need.
>

This was the case before we introduced the testpmd shell, which is a
standalone object (as in the dev uses the sut node and the test pmd
object to do what they need). One advantage of using sut methods to
instantiate testpmd shells is that devs won't need to import the
TestPmd shell, but I don't know which of these is going to be better.

> That being said, I think in any other framework the passing of the
> node into the shell would easily make more sense and I'm not opposed
> to going that route either. In general, I agree that not using a
> factory with a generic will make things much easier in the future.
>
> >
> > > The current approach appears to me to be top-down instead of bottom-up.
> > > We take the most abstracted part and we work our way down. But all we
> > > want is concreteness to the end user (developer).
> > >
> > > > Let me illustrate this on the TestPmdShell __init__() method I had in mind:
> > > >
> > > > def __init__(self, interactive_session: SSHClient,
> > > >          logger: DTSLogger,
> > > >          get_privileged_command: Callable[[str], str] | None,
> > > >          app_args: EalTestPmdParams | None = None,
> > > >          timeout: float = SETTINGS.timeout,
> > > >      ) -> None:
> > > >      super().__init__(interactive_session, logger, get_privileged_command)
> > > >      self.state = TestPmdState()
> > > >
> > > > Where EalTestPmdParams would be something that enforces that
> > > > app_args.app_params is of the TestPmdParameters type.
> > > >
> > > > But thinking more about this, we're probably better off switching the
> > > > params composition. Instead of TestPmdParameters being part of
> > > > EalParameters, we do it the other way around. This way the type of
> > > > app_args could just be TestPmdParameters and the types should work.
> > > > Or we pass the args separately, but that would likely require ditching
> > > > the factories and replacing them with methods (or overloading them).
> > > >
> > > > And hopefully the imports won't be impossible to solve. :-)
> > >
> > > It is what I feared, and I think it may become even more convoluted. As
> > > you said, ditching the factories will simplify things and make it more
> > > straightforward. So, we wouldn't find ourselves in problems like these.
> > >
> > > I don't have a strong preference in approach between:
> > > * overloading node methods
> > > * dedicated node methods
> > > * let the shells ingest nodes instead
> > >
> > > But if I were to give priority, I'd take it from last to first. Letting
> > > shells ingest nodes will decouple the situation adding an extra step of
> > > simplification.
> >
> > +1 for simplification.
> >
> > > I may not see the full picture though. The two are
> > > reasonable but, having a dedicated node method will stop the requirement
> > > to import the shell we need, and it's pretty much equivalent... but
> > > overloading also is very new to Python, so I may prefer to stick to more
> > > established.
> > >
> >
> > Let's try shells ingesting nodes if the imports work out then. If not,
> > we can fall back to dedicated node methods.
> >
> > > Letting TestPmdParams take EalParams, instead of the other way around,
> > > would naturally follow the bottom-up approach too. Allowing Params to
> > > arbitrarily append string arguments – as proposed, would also allow
> > > users to use a plain (EalParams + string). So sounds like a good
> > > approach overall.
>
> This I like a lot. We don't want to force EalParams to have
> TestpmdParams nested inside of them because other DPDK apps might need
> them too and this fixes the issue of always having to assert what type
> of inner params you have.

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 5/6] dts: add statefulness to InteractiveShell
  2024-04-10 13:35         ` Juraj Linkeš
  2024-04-10 14:07           ` Luca Vizzarro
@ 2024-04-29 14:48           ` Jeremy Spewock
  1 sibling, 0 replies; 159+ messages in thread
From: Jeremy Spewock @ 2024-04-29 14:48 UTC (permalink / raw)
  To: Juraj Linkeš
  Cc: Luca Vizzarro, dev, Jack Bond-Preston, Honnappa Nagarahalli

On Wed, Apr 10, 2024 at 9:36 AM Juraj Linkeš <juraj.linkes@pantheon.tech> wrote:
>
> On Wed, Apr 10, 2024 at 1:27 PM Luca Vizzarro <Luca.Vizzarro@arm.com> wrote:
> >
> > On 10/04/2024 07:53, Juraj Linkeš wrote:
> > > I have a general question. What are these changes for? Do you
> > > anticipate us needing this in the future? Wouldn't it be better to add
> > > it only when we need it?
> >
> > It's been sometime since we raised this task internally. This patch and
> > the next one arise from some survey done on old DTS test cases.
> > Unfortunately, I can't pinpoint.
> >
> > Specifically for this patch though, the timeout bit is useful in
> > conjunction with the related change in the next. Instead of giving an
> > optional timeout argument to all the commands where we may want to
> > change it, aren't we better off with providing a facility to temporarily
> > change this for the current scope?
> >
>
> This is a good question. If the scope is just one command, then no. If
> it's more than one, then maybe yes. I don't know which is better.
>
> We should also consider that this would introduce a difference in API
> between the interactive and non-interactive sessions. Do we want to do
> this there as well?

I believe there already is a difference in this case since the
interactive shell doesn't support modification of timeout on a
per-command basis. This is mainly because the way interactive shells
handle timeouts is on a lower level than sending a command using
fabric. Currently the interactive shells are modifying the timeout on
the channel of the connection, whereas fabric supports a keyword
argument that can modify timeouts on a per-command basis.

Of course we could also change the interactive shell send_command to
modify the timeout of the shell, but something else to note here is
that changing the timeout of the channel of the connection is slightly
different than giving a timeout for a command. This is because when
you change the timeout of the channel you're setting the timeout for
read/write operations on that channel. So, if you send a command and
give a timeout of 5 seconds for example, as long as you are receiving
output from the shell at least every 5 seconds, the command actually
wouldn't ever timeout. If we want to make the interactive shell
support passing a timeout per command, I would recommend we do it in a
different way that is more representative of your *command* having a
timeout instead of the shell's channel having a timeout.

Waiting for the status of a link to be "up" in testpmd was an
exception where I did allow you to give a specific timeout for the
command and this is exactly for the reason above. I wanted to make
sure that the user was able to specify how long they wanted to wait
for this status to be what they expect as opposed to how long to wait
for getting output from the channel. This is not supported in any
other method of the interactive shell.

>
> Also, maybe set_timeout should be a property or we could just make
> _timeout public.
> And is_privileged should just be privileged, as it's a property (which
> shouldn't contain a verb; if it was a method it would be a good name).
<snip>
> >

^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v2 0/8] dts: add testpmd params
  2024-03-26 19:04 [PATCH 0/6] dts: add testpmd params and statefulness Luca Vizzarro
                   ` (5 preceding siblings ...)
  2024-03-26 19:04 ` [PATCH 6/6] dts: add statefulness to TestPmdShell Luca Vizzarro
@ 2024-05-09 11:20 ` Luca Vizzarro
  2024-05-09 11:20   ` [PATCH v2 1/8] dts: add params manipulation module Luca Vizzarro
                     ` (8 more replies)
  2024-05-30 15:24 ` [PATCH v3 " Luca Vizzarro
                   ` (4 subsequent siblings)
  11 siblings, 9 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-05-09 11:20 UTC (permalink / raw)
  To: dev; +Cc: Juraj Linkeš, Jeremy Spewock, Luca Vizzarro

Hello,

sending in v2:
- refactored the params module
- strengthened typing of the params module
- moved the params module into its own package
- refactored EalParams and TestPmdParams and
  moved under the params package
- reworked interactions between nodes and shells
- refactored imports leading to circular dependencies

Best,
Luca

---
Depends-on: series-31896 ("dts: update mypy and clean up")
---

Luca Vizzarro (8):
  dts: add params manipulation module
  dts: use Params for interactive shells
  dts: refactor EalParams
  dts: remove module-wide imports
  dts: add testpmd shell params
  dts: use testpmd params for scatter test suite
  dts: rework interactive shells
  dts: use Unpack for type checking and hinting

 dts/framework/params/__init__.py              | 274 ++++++++
 dts/framework/params/eal.py                   |  50 ++
 dts/framework/params/testpmd.py               | 608 ++++++++++++++++++
 dts/framework/params/types.py                 | 133 ++++
 dts/framework/remote_session/__init__.py      |   5 +-
 dts/framework/remote_session/dpdk_shell.py    | 104 +++
 .../remote_session/interactive_shell.py       |  83 ++-
 dts/framework/remote_session/python_shell.py  |   4 +-
 dts/framework/remote_session/testpmd_shell.py | 102 ++-
 dts/framework/runner.py                       |   4 +-
 dts/framework/test_suite.py                   |   5 +-
 dts/framework/testbed_model/__init__.py       |   7 -
 dts/framework/testbed_model/node.py           |  36 +-
 dts/framework/testbed_model/os_session.py     |  38 +-
 dts/framework/testbed_model/sut_node.py       | 182 +-----
 .../testbed_model/traffic_generator/scapy.py  |   6 +-
 dts/tests/TestSuite_hello_world.py            |   9 +-
 dts/tests/TestSuite_pmd_buffer_scatter.py     |  21 +-
 dts/tests/TestSuite_smoke_tests.py            |   4 +-
 19 files changed, 1296 insertions(+), 379 deletions(-)
 create mode 100644 dts/framework/params/__init__.py
 create mode 100644 dts/framework/params/eal.py
 create mode 100644 dts/framework/params/testpmd.py
 create mode 100644 dts/framework/params/types.py
 create mode 100644 dts/framework/remote_session/dpdk_shell.py

-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v2 1/8] dts: add params manipulation module
  2024-05-09 11:20 ` [PATCH v2 0/8] dts: add testpmd params Luca Vizzarro
@ 2024-05-09 11:20   ` Luca Vizzarro
  2024-05-28 15:40     ` Nicholas Pratte
                       ` (2 more replies)
  2024-05-09 11:20   ` [PATCH v2 2/8] dts: use Params for interactive shells Luca Vizzarro
                     ` (7 subsequent siblings)
  8 siblings, 3 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-05-09 11:20 UTC (permalink / raw)
  To: dev; +Cc: Juraj Linkeš, Jeremy Spewock, Luca Vizzarro, Paul Szczepanek

This commit introduces a new "params" module, which adds a new way
to manage command line parameters. The provided Params dataclass
is able to read the fields of its child class and produce a string
representation to supply to the command line. Any data structure
that is intended to represent command line parameters can inherit it.

The main purpose is to make it easier to represent data structures that
map to parameters. Aiding quicker development, while minimising code
bloat.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 dts/framework/params/__init__.py | 274 +++++++++++++++++++++++++++++++
 1 file changed, 274 insertions(+)
 create mode 100644 dts/framework/params/__init__.py

diff --git a/dts/framework/params/__init__.py b/dts/framework/params/__init__.py
new file mode 100644
index 0000000000..aa27e34357
--- /dev/null
+++ b/dts/framework/params/__init__.py
@@ -0,0 +1,274 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Parameter manipulation module.
+
+This module provides :class:`Params` which can be used to model any data structure
+that is meant to represent any command parameters.
+"""
+
+from dataclasses import dataclass, fields
+from enum import Flag
+from typing import Any, Callable, Iterable, Literal, Reversible, TypedDict, cast
+
+from typing_extensions import Self
+
+#: Type for a function taking one argument.
+FnPtr = Callable[[Any], Any]
+#: Type for a switch parameter.
+Switch = Literal[True, None]
+#: Type for a yes/no switch parameter.
+YesNoSwitch = Literal[True, False, None]
+
+
+def _reduce_functions(funcs: Reversible[FnPtr]) -> FnPtr:
+    """Reduces an iterable of :attr:`FnPtr` from end to start to a composite function.
+
+    If the iterable is empty, the created function just returns its fed value back.
+    """
+
+    def composite_function(value: Any):
+        for fn in reversed(funcs):
+            value = fn(value)
+        return value
+
+    return composite_function
+
+
+def convert_str(*funcs: FnPtr):
+    """Decorator that makes the ``__str__`` method a composite function created from its arguments.
+
+    The :attr:`FnPtr`s fed to the decorator are executed from right to left
+    in the arguments list order.
+
+    Example:
+    .. code:: python
+
+        @convert_str(hex_from_flag_value)
+        class BitMask(enum.Flag):
+            A = auto()
+            B = auto()
+
+    will allow ``BitMask`` to render as a hexadecimal value.
+    """
+
+    def _class_decorator(original_class):
+        original_class.__str__ = _reduce_functions(funcs)
+        return original_class
+
+    return _class_decorator
+
+
+def comma_separated(values: Iterable[Any]) -> str:
+    """Converts an iterable in a comma-separated string."""
+    return ",".join([str(value).strip() for value in values if value is not None])
+
+
+def bracketed(value: str) -> str:
+    """Adds round brackets to the input."""
+    return f"({value})"
+
+
+def str_from_flag_value(flag: Flag) -> str:
+    """Returns the value from a :class:`enum.Flag` as a string."""
+    return str(flag.value)
+
+
+def hex_from_flag_value(flag: Flag) -> str:
+    """Returns the value from a :class:`enum.Flag` converted to hexadecimal."""
+    return hex(flag.value)
+
+
+class ParamsModifier(TypedDict, total=False):
+    """Params modifiers dict compatible with the :func:`dataclasses.field` metadata parameter."""
+
+    #:
+    Params_value_only: bool
+    #:
+    Params_short: str
+    #:
+    Params_long: str
+    #:
+    Params_multiple: bool
+    #:
+    Params_convert_value: Reversible[FnPtr]
+
+
+@dataclass
+class Params:
+    """Dataclass that renders its fields into command line arguments.
+
+    The parameter name is taken from the field name by default. The following:
+
+    .. code:: python
+
+        name: str | None = "value"
+
+    is rendered as ``--name=value``.
+    Through :func:`dataclasses.field` the resulting parameter can be manipulated by applying
+    this class' metadata modifier functions.
+
+    To use fields as switches, set the value to ``True`` to render them. If you
+    use a yes/no switch you can also set ``False`` which would render a switch
+    prefixed with ``--no-``. Examples:
+
+    .. code:: python
+
+        interactive: Switch = True  # renders --interactive
+        numa: YesNoSwitch   = False # renders --no-numa
+
+    Setting ``None`` will prevent it from being rendered. The :attr:`~Switch` type alias is provided
+    for regular switches, whereas :attr:`~YesNoSwitch` is offered for yes/no ones.
+
+    An instance of a dataclass inheriting ``Params`` can also be assigned to an attribute,
+    this helps with grouping parameters together.
+    The attribute holding the dataclass will be ignored and the latter will just be rendered as
+    expected.
+    """
+
+    _suffix = ""
+    """Holder of the plain text value of Params when called directly. A suffix for child classes."""
+
+    """========= BEGIN FIELD METADATA MODIFIER FUNCTIONS ========"""
+
+    @staticmethod
+    def value_only() -> ParamsModifier:
+        """Injects the value of the attribute as-is without flag.
+
+        Metadata modifier for :func:`dataclasses.field`.
+        """
+        return ParamsModifier(Params_value_only=True)
+
+    @staticmethod
+    def short(name: str) -> ParamsModifier:
+        """Overrides any parameter name with the given short option.
+
+        Metadata modifier for :func:`dataclasses.field`.
+
+        Example:
+        .. code:: python
+
+            logical_cores: str | None = field(default="1-4", metadata=Params.short("l"))
+
+        will render as ``-l=1-4`` instead of ``--logical-cores=1-4``.
+        """
+        return ParamsModifier(Params_short=name)
+
+    @staticmethod
+    def long(name: str) -> ParamsModifier:
+        """Overrides the inferred parameter name to the specified one.
+
+        Metadata modifier for :func:`dataclasses.field`.
+
+        Example:
+        .. code:: python
+
+            x_name: str | None = field(default="y", metadata=Params.long("x"))
+
+        will render as ``--x=y``, but the field is accessed and modified through ``x_name``.
+        """
+        return ParamsModifier(Params_long=name)
+
+    @staticmethod
+    def multiple() -> ParamsModifier:
+        """Specifies that this parameter is set multiple times. Must be a list.
+
+        Metadata modifier for :func:`dataclasses.field`.
+
+        Example:
+        .. code:: python
+
+            ports: list[int] | None = field(
+                default_factory=lambda: [0, 1, 2],
+                metadata=Params.multiple() | Params.long("port")
+            )
+
+        will render as ``--port=0 --port=1 --port=2``. Note that modifiers can be chained like
+        in this example.
+        """
+        return ParamsModifier(Params_multiple=True)
+
+    @classmethod
+    def convert_value(cls, *funcs: FnPtr) -> ParamsModifier:
+        """Takes in a variable number of functions to convert the value text representation.
+
+        Metadata modifier for :func:`dataclasses.field`.
+
+        The ``metadata`` keyword argument can be used to chain metadata modifiers together.
+
+        Functions can be chained together, executed from right to left in the arguments list order.
+
+        Example:
+        .. code:: python
+
+            hex_bitmask: int | None = field(
+                default=0b1101,
+                metadata=Params.convert_value(hex) | Params.long("mask")
+            )
+
+        will render as ``--mask=0xd``.
+        """
+        return ParamsModifier(Params_convert_value=funcs)
+
+    """========= END FIELD METADATA MODIFIER FUNCTIONS ========"""
+
+    def append_str(self, text: str) -> None:
+        """Appends a string at the end of the string representation."""
+        self._suffix += text
+
+    def __iadd__(self, text: str) -> Self:
+        """Appends a string at the end of the string representation."""
+        self.append_str(text)
+        return self
+
+    @classmethod
+    def from_str(cls, text: str) -> Self:
+        """Creates a plain Params object from a string."""
+        obj = cls()
+        obj.append_str(text)
+        return obj
+
+    @staticmethod
+    def _make_switch(
+        name: str, is_short: bool = False, is_no: bool = False, value: str | None = None
+    ) -> str:
+        prefix = f"{'-' if is_short else '--'}{'no-' if is_no else ''}"
+        name = name.replace("_", "-")
+        value = f"{' ' if is_short else '='}{value}" if value else ""
+        return f"{prefix}{name}{value}"
+
+    def __str__(self) -> str:
+        """Returns a string of command-line-ready arguments from the class fields."""
+        arguments: list[str] = []
+
+        for field in fields(self):
+            value = getattr(self, field.name)
+            modifiers = cast(ParamsModifier, field.metadata)
+
+            if value is None:
+                continue
+
+            value_only = modifiers.get("Params_value_only", False)
+            if isinstance(value, Params) or value_only:
+                arguments.append(str(value))
+                continue
+
+            # take the short modifier, or the long modifier, or infer from field name
+            switch_name = modifiers.get("Params_short", modifiers.get("Params_long", field.name))
+            is_short = "Params_short" in modifiers
+
+            if isinstance(value, bool):
+                arguments.append(self._make_switch(switch_name, is_short, is_no=(not value)))
+                continue
+
+            convert = _reduce_functions(modifiers.get("Params_convert_value", []))
+            multiple = modifiers.get("Params_multiple", False)
+
+            values = value if multiple else [value]
+            for value in values:
+                arguments.append(self._make_switch(switch_name, is_short, value=convert(value)))
+
+        if self._suffix:
+            arguments.append(self._suffix)
+
+        return " ".join(arguments)
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v2 2/8] dts: use Params for interactive shells
  2024-05-09 11:20 ` [PATCH v2 0/8] dts: add testpmd params Luca Vizzarro
  2024-05-09 11:20   ` [PATCH v2 1/8] dts: add params manipulation module Luca Vizzarro
@ 2024-05-09 11:20   ` Luca Vizzarro
  2024-05-28 17:43     ` Nicholas Pratte
                       ` (2 more replies)
  2024-05-09 11:20   ` [PATCH v2 3/8] dts: refactor EalParams Luca Vizzarro
                     ` (6 subsequent siblings)
  8 siblings, 3 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-05-09 11:20 UTC (permalink / raw)
  To: dev; +Cc: Juraj Linkeš, Jeremy Spewock, Luca Vizzarro, Paul Szczepanek

Make it so that interactive shells accept an implementation of `Params`
for app arguments. Convert EalParameters to use `Params` instead.

String command line parameters can still be supplied by using the
`Params.from_str()` method.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 .../remote_session/interactive_shell.py       |  12 +-
 dts/framework/remote_session/testpmd_shell.py |  11 +-
 dts/framework/testbed_model/node.py           |   6 +-
 dts/framework/testbed_model/os_session.py     |   4 +-
 dts/framework/testbed_model/sut_node.py       | 124 ++++++++----------
 dts/tests/TestSuite_pmd_buffer_scatter.py     |   3 +-
 6 files changed, 77 insertions(+), 83 deletions(-)

diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
index 074a541279..9da66d1c7e 100644
--- a/dts/framework/remote_session/interactive_shell.py
+++ b/dts/framework/remote_session/interactive_shell.py
@@ -1,5 +1,6 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright(c) 2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """Common functionality for interactive shell handling.
 
@@ -21,6 +22,7 @@
 from paramiko import Channel, SSHClient, channel  # type: ignore[import-untyped]
 
 from framework.logger import DTSLogger
+from framework.params import Params
 from framework.settings import SETTINGS
 
 
@@ -40,7 +42,7 @@ class InteractiveShell(ABC):
     _ssh_channel: Channel
     _logger: DTSLogger
     _timeout: float
-    _app_args: str
+    _app_params: Params
 
     #: Prompt to expect at the end of output when sending a command.
     #: This is often overridden by subclasses.
@@ -63,7 +65,7 @@ def __init__(
         interactive_session: SSHClient,
         logger: DTSLogger,
         get_privileged_command: Callable[[str], str] | None,
-        app_args: str = "",
+        app_params: Params = Params(),
         timeout: float = SETTINGS.timeout,
     ) -> None:
         """Create an SSH channel during initialization.
@@ -74,7 +76,7 @@ def __init__(
             get_privileged_command: A method for modifying a command to allow it to use
                 elevated privileges. If :data:`None`, the application will not be started
                 with elevated privileges.
-            app_args: The command line arguments to be passed to the application on startup.
+            app_params: The command line parameters to be passed to the application on startup.
             timeout: The timeout used for the SSH channel that is dedicated to this interactive
                 shell. This timeout is for collecting output, so if reading from the buffer
                 and no output is gathered within the timeout, an exception is thrown.
@@ -87,7 +89,7 @@ def __init__(
         self._ssh_channel.set_combine_stderr(True)  # combines stdout and stderr streams
         self._logger = logger
         self._timeout = timeout
-        self._app_args = app_args
+        self._app_params = app_params
         self._start_application(get_privileged_command)
 
     def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
@@ -100,7 +102,7 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
             get_privileged_command: A function (but could be any callable) that produces
                 the version of the command with elevated privileges.
         """
-        start_command = f"{self.path} {self._app_args}"
+        start_command = f"{self.path} {self._app_params}"
         if get_privileged_command is not None:
             start_command = get_privileged_command(start_command)
         self.send_command(start_command)
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index cb2ab6bd00..7eced27096 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -22,6 +22,7 @@
 
 from framework.exception import InteractiveCommandExecutionError
 from framework.settings import SETTINGS
+from framework.testbed_model.sut_node import EalParams
 from framework.utils import StrEnum
 
 from .interactive_shell import InteractiveShell
@@ -118,8 +119,14 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
         Also find the number of pci addresses which were allowed on the command line when the app
         was started.
         """
-        self._app_args += " -i --mask-event intr_lsc"
-        self.number_of_ports = self._app_args.count("-a ")
+        self._app_params += " -i --mask-event intr_lsc"
+
+        assert isinstance(self._app_params, EalParams)
+
+        self.number_of_ports = (
+            len(self._app_params.ports) if self._app_params.ports is not None else 0
+        )
+
         super()._start_application(get_privileged_command)
 
     def start(self, verify: bool = True) -> None:
diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
index 74061f6262..6af4f25a3c 100644
--- a/dts/framework/testbed_model/node.py
+++ b/dts/framework/testbed_model/node.py
@@ -2,6 +2,7 @@
 # Copyright(c) 2010-2014 Intel Corporation
 # Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2022-2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """Common functionality for node management.
 
@@ -24,6 +25,7 @@
 )
 from framework.exception import ConfigurationError
 from framework.logger import DTSLogger, get_dts_logger
+from framework.params import Params
 from framework.settings import SETTINGS
 
 from .cpu import (
@@ -199,7 +201,7 @@ def create_interactive_shell(
         shell_cls: Type[InteractiveShellType],
         timeout: float = SETTINGS.timeout,
         privileged: bool = False,
-        app_args: str = "",
+        app_params: Params = Params(),
     ) -> InteractiveShellType:
         """Factory for interactive session handlers.
 
@@ -222,7 +224,7 @@ def create_interactive_shell(
             shell_cls,
             timeout,
             privileged,
-            app_args,
+            app_params,
         )
 
     def filter_lcores(
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
index d5bf7e0401..1a77aee532 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright(c) 2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """OS-aware remote session.
 
@@ -29,6 +30,7 @@
 
 from framework.config import Architecture, NodeConfiguration, NodeInfo
 from framework.logger import DTSLogger
+from framework.params import Params
 from framework.remote_session import (
     CommandResult,
     InteractiveRemoteSession,
@@ -134,7 +136,7 @@ def create_interactive_shell(
         shell_cls: Type[InteractiveShellType],
         timeout: float,
         privileged: bool,
-        app_args: str,
+        app_args: Params,
     ) -> InteractiveShellType:
         """Factory for interactive session handlers.
 
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index 97aa26d419..c886590979 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -2,6 +2,7 @@
 # Copyright(c) 2010-2014 Intel Corporation
 # Copyright(c) 2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """System under test (DPDK + hardware) node.
 
@@ -14,8 +15,9 @@
 import os
 import tarfile
 import time
+from dataclasses import dataclass, field
 from pathlib import PurePath
-from typing import Type
+from typing import Literal, Type
 
 from framework.config import (
     BuildTargetConfiguration,
@@ -23,6 +25,7 @@
     NodeInfo,
     SutNodeConfiguration,
 )
+from framework.params import Params, Switch
 from framework.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
@@ -34,62 +37,42 @@
 from .virtual_device import VirtualDevice
 
 
-class EalParameters(object):
-    """The environment abstraction layer parameters.
-
-    The string representation can be created by converting the instance to a string.
-    """
+def _port_to_pci(port: Port) -> str:
+    return port.pci
 
-    def __init__(
-        self,
-        lcore_list: LogicalCoreList,
-        memory_channels: int,
-        prefix: str,
-        no_pci: bool,
-        vdevs: list[VirtualDevice],
-        ports: list[Port],
-        other_eal_param: str,
-    ):
-        """Initialize the parameters according to inputs.
-
-        Process the parameters into the format used on the command line.
 
-        Args:
-            lcore_list: The list of logical cores to use.
-            memory_channels: The number of memory channels to use.
-            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
-            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
-            vdevs: Virtual devices, e.g.::
+@dataclass(kw_only=True)
+class EalParams(Params):
+    """The environment abstraction layer parameters.
 
-                vdevs=[
-                    VirtualDevice('net_ring0'),
-                    VirtualDevice('net_ring1')
-                ]
-            ports: The list of ports to allow.
-            other_eal_param: user defined DPDK EAL parameters, e.g.:
+    Attributes:
+        lcore_list: The list of logical cores to use.
+        memory_channels: The number of memory channels to use.
+        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
+        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
+        vdevs: Virtual devices, e.g.::
+            vdevs=[
+                VirtualDevice('net_ring0'),
+                VirtualDevice('net_ring1')
+            ]
+        ports: The list of ports to allow.
+        other_eal_param: user defined DPDK EAL parameters, e.g.:
                 ``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._ports = " ".join(f"-a {port.pci}" for port in ports)
-        self._other_eal_param = other_eal_param
-
-    def __str__(self) -> str:
-        """Create the EAL string."""
-        return (
-            f"{self._lcore_list} "
-            f"{self._memory_channels} "
-            f"{self._prefix} "
-            f"{self._no_pci} "
-            f"{self._vdevs} "
-            f"{self._ports} "
-            f"{self._other_eal_param}"
-        )
+    """
+
+    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
+    memory_channels: int = field(metadata=Params.short("n"))
+    prefix: str = field(metadata=Params.long("file-prefix"))
+    no_pci: Switch
+    vdevs: list[VirtualDevice] | None = field(
+        default=None, metadata=Params.multiple() | Params.long("vdev")
+    )
+    ports: list[Port] | None = field(
+        default=None,
+        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
+    )
+    other_eal_param: Params | None = None
+    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
 
 
 class SutNode(Node):
@@ -350,11 +333,11 @@ def create_eal_parameters(
         ascending_cores: bool = True,
         prefix: str = "dpdk",
         append_prefix_timestamp: bool = True,
-        no_pci: bool = False,
+        no_pci: Switch = None,
         vdevs: list[VirtualDevice] | None = None,
         ports: list[Port] | None = None,
         other_eal_param: str = "",
-    ) -> "EalParameters":
+    ) -> EalParams:
         """Compose the EAL parameters.
 
         Process the list of cores and the DPDK prefix and pass that along with
@@ -393,24 +376,21 @@ def create_eal_parameters(
         if prefix:
             self._dpdk_prefix_list.append(prefix)
 
-        if vdevs is None:
-            vdevs = []
-
         if ports is None:
             ports = self.ports
 
-        return EalParameters(
+        return EalParams(
             lcore_list=lcore_list,
             memory_channels=self.config.memory_channels,
             prefix=prefix,
             no_pci=no_pci,
             vdevs=vdevs,
             ports=ports,
-            other_eal_param=other_eal_param,
+            other_eal_param=Params.from_str(other_eal_param),
         )
 
     def run_dpdk_app(
-        self, app_path: PurePath, eal_args: "EalParameters", timeout: float = 30
+        self, app_path: PurePath, eal_params: EalParams, timeout: float = 30
     ) -> CommandResult:
         """Run DPDK application on the remote node.
 
@@ -419,14 +399,14 @@ def run_dpdk_app(
 
         Args:
             app_path: The remote path to the DPDK application.
-            eal_args: EAL parameters to run the DPDK application with.
+            eal_params: EAL parameters to run the DPDK application with.
             timeout: Wait at most this long in seconds for `command` execution to complete.
 
         Returns:
             The result of the DPDK app execution.
         """
         return self.main_session.send_command(
-            f"{app_path} {eal_args}", timeout, privileged=True, verify=True
+            f"{app_path} {eal_params}", timeout, privileged=True, verify=True
         )
 
     def configure_ipv4_forwarding(self, enable: bool) -> None:
@@ -442,8 +422,8 @@ def create_interactive_shell(
         shell_cls: Type[InteractiveShellType],
         timeout: float = SETTINGS.timeout,
         privileged: bool = False,
-        app_parameters: str = "",
-        eal_parameters: EalParameters | None = None,
+        app_params: Params = Params(),
+        eal_params: EalParams | None = None,
     ) -> InteractiveShellType:
         """Extend the factory for interactive session handlers.
 
@@ -459,26 +439,26 @@ def create_interactive_shell(
                 reading from the buffer and don't receive any data within the timeout
                 it will throw an error.
             privileged: Whether to run the shell with administrative privileges.
-            eal_parameters: List of EAL parameters to use to launch the app. If this
+            app_params: The parameters to be passed to the application.
+            eal_params: List of EAL parameters to use to launch the app. If this
                 isn't provided or an empty string is passed, it will default to calling
                 :meth:`create_eal_parameters`.
-            app_parameters: Additional arguments to pass into the application on the
-                command-line.
 
         Returns:
             An instance of the desired interactive application shell.
         """
         # We need to append the build directory and add EAL parameters for DPDK apps
         if shell_cls.dpdk_app:
-            if not eal_parameters:
-                eal_parameters = self.create_eal_parameters()
-            app_parameters = f"{eal_parameters} -- {app_parameters}"
+            if eal_params is None:
+                eal_params = self.create_eal_parameters()
+            eal_params.append_str(str(app_params))
+            app_params = eal_params
 
             shell_cls.path = self.main_session.join_remote_path(
                 self.remote_dpdk_build_dir, shell_cls.path
             )
 
-        return super().create_interactive_shell(shell_cls, timeout, privileged, app_parameters)
+        return super().create_interactive_shell(shell_cls, timeout, privileged, app_params)
 
     def bind_ports_to_driver(self, for_dpdk: bool = True) -> None:
         """Bind all ports on the SUT to a driver.
diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index a020682e8d..c6e93839cb 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -22,6 +22,7 @@
 from scapy.packet import Raw  # type: ignore[import-untyped]
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
+from framework.params import Params
 from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
 from framework.test_suite import TestSuite
 
@@ -103,7 +104,7 @@ def pmd_scatter(self, mbsize: int) -> None:
         """
         testpmd = self.sut_node.create_interactive_shell(
             TestPmdShell,
-            app_parameters=(
+            app_params=Params.from_str(
                 "--mbcache=200 "
                 f"--mbuf-size={mbsize} "
                 "--max-pkt-len=9000 "
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v2 3/8] dts: refactor EalParams
  2024-05-09 11:20 ` [PATCH v2 0/8] dts: add testpmd params Luca Vizzarro
  2024-05-09 11:20   ` [PATCH v2 1/8] dts: add params manipulation module Luca Vizzarro
  2024-05-09 11:20   ` [PATCH v2 2/8] dts: use Params for interactive shells Luca Vizzarro
@ 2024-05-09 11:20   ` Luca Vizzarro
  2024-05-28 15:44     ` Nicholas Pratte
                       ` (2 more replies)
  2024-05-09 11:20   ` [PATCH v2 4/8] dts: remove module-wide imports Luca Vizzarro
                     ` (5 subsequent siblings)
  8 siblings, 3 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-05-09 11:20 UTC (permalink / raw)
  To: dev; +Cc: Juraj Linkeš, Jeremy Spewock, Luca Vizzarro, Paul Szczepanek

Move EalParams to its own module to avoid circular dependencies.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 dts/framework/params/eal.py                   | 50 +++++++++++++++++++
 dts/framework/remote_session/testpmd_shell.py |  2 +-
 dts/framework/testbed_model/sut_node.py       | 42 +---------------
 3 files changed, 53 insertions(+), 41 deletions(-)
 create mode 100644 dts/framework/params/eal.py

diff --git a/dts/framework/params/eal.py b/dts/framework/params/eal.py
new file mode 100644
index 0000000000..bbdbc8f334
--- /dev/null
+++ b/dts/framework/params/eal.py
@@ -0,0 +1,50 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Module representing the DPDK EAL-related parameters."""
+
+from dataclasses import dataclass, field
+from typing import Literal
+
+from framework.params import Params, Switch
+from framework.testbed_model.cpu import LogicalCoreList
+from framework.testbed_model.port import Port
+from framework.testbed_model.virtual_device import VirtualDevice
+
+
+def _port_to_pci(port: Port) -> str:
+    return port.pci
+
+
+@dataclass(kw_only=True)
+class EalParams(Params):
+    """The environment abstraction layer parameters.
+
+    Attributes:
+        lcore_list: The list of logical cores to use.
+        memory_channels: The number of memory channels to use.
+        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
+        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
+        vdevs: Virtual devices, e.g.::
+            vdevs=[
+                VirtualDevice('net_ring0'),
+                VirtualDevice('net_ring1')
+            ]
+        ports: The list of ports to allow.
+        other_eal_param: user defined DPDK EAL parameters, e.g.:
+                ``other_eal_param='--single-file-segments'``
+    """
+
+    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
+    memory_channels: int = field(metadata=Params.short("n"))
+    prefix: str = field(metadata=Params.long("file-prefix"))
+    no_pci: Switch = None
+    vdevs: list[VirtualDevice] | None = field(
+        default=None, metadata=Params.multiple() | Params.long("vdev")
+    )
+    ports: list[Port] | None = field(
+        default=None,
+        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
+    )
+    other_eal_param: Params | None = None
+    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index 7eced27096..841d456a2f 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -21,8 +21,8 @@
 from typing import Callable, ClassVar
 
 from framework.exception import InteractiveCommandExecutionError
+from framework.params.eal import EalParams
 from framework.settings import SETTINGS
-from framework.testbed_model.sut_node import EalParams
 from framework.utils import StrEnum
 
 from .interactive_shell import InteractiveShell
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index c886590979..e1163106a3 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -15,9 +15,8 @@
 import os
 import tarfile
 import time
-from dataclasses import dataclass, field
 from pathlib import PurePath
-from typing import Literal, Type
+from typing import Type
 
 from framework.config import (
     BuildTargetConfiguration,
@@ -26,6 +25,7 @@
     SutNodeConfiguration,
 )
 from framework.params import Params, Switch
+from framework.params.eal import EalParams
 from framework.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
@@ -37,44 +37,6 @@
 from .virtual_device import VirtualDevice
 
 
-def _port_to_pci(port: Port) -> str:
-    return port.pci
-
-
-@dataclass(kw_only=True)
-class EalParams(Params):
-    """The environment abstraction layer parameters.
-
-    Attributes:
-        lcore_list: The list of logical cores to use.
-        memory_channels: The number of memory channels to use.
-        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
-        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
-        vdevs: Virtual devices, e.g.::
-            vdevs=[
-                VirtualDevice('net_ring0'),
-                VirtualDevice('net_ring1')
-            ]
-        ports: The list of ports to allow.
-        other_eal_param: user defined DPDK EAL parameters, e.g.:
-                ``other_eal_param='--single-file-segments'``
-    """
-
-    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
-    memory_channels: int = field(metadata=Params.short("n"))
-    prefix: str = field(metadata=Params.long("file-prefix"))
-    no_pci: Switch
-    vdevs: list[VirtualDevice] | None = field(
-        default=None, metadata=Params.multiple() | Params.long("vdev")
-    )
-    ports: list[Port] | None = field(
-        default=None,
-        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
-    )
-    other_eal_param: Params | None = None
-    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
-
-
 class SutNode(Node):
     """The system under test node.
 
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v2 4/8] dts: remove module-wide imports
  2024-05-09 11:20 ` [PATCH v2 0/8] dts: add testpmd params Luca Vizzarro
                     ` (2 preceding siblings ...)
  2024-05-09 11:20   ` [PATCH v2 3/8] dts: refactor EalParams Luca Vizzarro
@ 2024-05-09 11:20   ` Luca Vizzarro
  2024-05-28 15:45     ` Nicholas Pratte
                       ` (2 more replies)
  2024-05-09 11:20   ` [PATCH v2 5/8] dts: add testpmd shell params Luca Vizzarro
                     ` (4 subsequent siblings)
  8 siblings, 3 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-05-09 11:20 UTC (permalink / raw)
  To: dev; +Cc: Juraj Linkeš, Jeremy Spewock, Luca Vizzarro, Paul Szczepanek

Remove the imports in the testbed_model and remote_session modules init
file, to avoid the initialisation of unneeded modules, thus removing or
limiting the risk of circular dependencies.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 dts/framework/remote_session/__init__.py               | 5 +----
 dts/framework/runner.py                                | 4 +++-
 dts/framework/test_suite.py                            | 5 ++++-
 dts/framework/testbed_model/__init__.py                | 7 -------
 dts/framework/testbed_model/os_session.py              | 4 ++--
 dts/framework/testbed_model/sut_node.py                | 2 +-
 dts/framework/testbed_model/traffic_generator/scapy.py | 2 +-
 dts/tests/TestSuite_hello_world.py                     | 2 +-
 dts/tests/TestSuite_smoke_tests.py                     | 2 +-
 9 files changed, 14 insertions(+), 19 deletions(-)

diff --git a/dts/framework/remote_session/__init__.py b/dts/framework/remote_session/__init__.py
index 1910c81c3c..29000a4642 100644
--- a/dts/framework/remote_session/__init__.py
+++ b/dts/framework/remote_session/__init__.py
@@ -18,11 +18,8 @@
 from framework.logger import DTSLogger
 
 from .interactive_remote_session import InteractiveRemoteSession
-from .interactive_shell import InteractiveShell
-from .python_shell import PythonShell
-from .remote_session import CommandResult, RemoteSession
+from .remote_session import RemoteSession
 from .ssh_session import SSHSession
-from .testpmd_shell import TestPmdShell
 
 
 def create_remote_session(
diff --git a/dts/framework/runner.py b/dts/framework/runner.py
index d74f1871db..e6c23af7c7 100644
--- a/dts/framework/runner.py
+++ b/dts/framework/runner.py
@@ -26,6 +26,9 @@
 from types import FunctionType
 from typing import Iterable, Sequence
 
+from framework.testbed_model.sut_node import SutNode
+from framework.testbed_model.tg_node import TGNode
+
 from .config import (
     BuildTargetConfiguration,
     Configuration,
@@ -51,7 +54,6 @@
     TestSuiteWithCases,
 )
 from .test_suite import TestSuite
-from .testbed_model import SutNode, TGNode
 
 
 class DTSRunner:
diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py
index 8768f756a6..9d3debb00f 100644
--- a/dts/framework/test_suite.py
+++ b/dts/framework/test_suite.py
@@ -20,9 +20,12 @@
 from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
 from scapy.packet import Packet, Padding  # type: ignore[import-untyped]
 
+from framework.testbed_model.port import Port, PortLink
+from framework.testbed_model.sut_node import SutNode
+from framework.testbed_model.tg_node import TGNode
+
 from .exception import TestCaseVerifyError
 from .logger import DTSLogger, get_dts_logger
-from .testbed_model import Port, PortLink, SutNode, TGNode
 from .testbed_model.traffic_generator import PacketFilteringConfig
 from .utils import get_packet_summaries
 
diff --git a/dts/framework/testbed_model/__init__.py b/dts/framework/testbed_model/__init__.py
index 6086512ca2..4f8a58c039 100644
--- a/dts/framework/testbed_model/__init__.py
+++ b/dts/framework/testbed_model/__init__.py
@@ -19,10 +19,3 @@
 """
 
 # pylama:ignore=W0611
-
-from .cpu import LogicalCoreCount, LogicalCoreCountFilter, LogicalCoreList
-from .node import Node
-from .port import Port, PortLink
-from .sut_node import SutNode
-from .tg_node import TGNode
-from .virtual_device import VirtualDevice
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
index 1a77aee532..e5f5fcbe0e 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -32,13 +32,13 @@
 from framework.logger import DTSLogger
 from framework.params import Params
 from framework.remote_session import (
-    CommandResult,
     InteractiveRemoteSession,
-    InteractiveShell,
     RemoteSession,
     create_interactive_session,
     create_remote_session,
 )
+from framework.remote_session.interactive_shell import InteractiveShell
+from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
 
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index e1163106a3..83ad06ae2d 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -26,7 +26,7 @@
 )
 from framework.params import Params, Switch
 from framework.params.eal import EalParams
-from framework.remote_session import CommandResult
+from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
 
diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py
index ed5467d825..7bc1c2cc08 100644
--- a/dts/framework/testbed_model/traffic_generator/scapy.py
+++ b/dts/framework/testbed_model/traffic_generator/scapy.py
@@ -25,7 +25,7 @@
 from scapy.packet import Packet  # type: ignore[import-untyped]
 
 from framework.config import OS, ScapyTrafficGeneratorConfig
-from framework.remote_session import PythonShell
+from framework.remote_session.python_shell import PythonShell
 from framework.settings import SETTINGS
 from framework.testbed_model.node import Node
 from framework.testbed_model.port import Port
diff --git a/dts/tests/TestSuite_hello_world.py b/dts/tests/TestSuite_hello_world.py
index fd7ff1534d..0d6995f260 100644
--- a/dts/tests/TestSuite_hello_world.py
+++ b/dts/tests/TestSuite_hello_world.py
@@ -8,7 +8,7 @@
 """
 
 from framework.test_suite import TestSuite
-from framework.testbed_model import (
+from framework.testbed_model.cpu import (
     LogicalCoreCount,
     LogicalCoreCountFilter,
     LogicalCoreList,
diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py
index a553e89662..ca678f662d 100644
--- a/dts/tests/TestSuite_smoke_tests.py
+++ b/dts/tests/TestSuite_smoke_tests.py
@@ -15,7 +15,7 @@
 import re
 
 from framework.config import PortConfig
-from framework.remote_session import TestPmdShell
+from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.settings import SETTINGS
 from framework.test_suite import TestSuite
 from framework.utils import REGEX_FOR_PCI_ADDRESS
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v2 5/8] dts: add testpmd shell params
  2024-05-09 11:20 ` [PATCH v2 0/8] dts: add testpmd params Luca Vizzarro
                     ` (3 preceding siblings ...)
  2024-05-09 11:20   ` [PATCH v2 4/8] dts: remove module-wide imports Luca Vizzarro
@ 2024-05-09 11:20   ` Luca Vizzarro
  2024-05-28 15:53     ` Nicholas Pratte
  2024-05-28 21:05     ` Jeremy Spewock
  2024-05-09 11:20   ` [PATCH v2 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
                     ` (3 subsequent siblings)
  8 siblings, 2 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-05-09 11:20 UTC (permalink / raw)
  To: dev; +Cc: Juraj Linkeš, Jeremy Spewock, Luca Vizzarro, Paul Szczepanek

Implement all the testpmd shell parameters into a data structure.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 dts/framework/params/testpmd.py               | 608 ++++++++++++++++++
 dts/framework/remote_session/testpmd_shell.py |  42 +-
 dts/tests/TestSuite_pmd_buffer_scatter.py     |   5 +-
 3 files changed, 615 insertions(+), 40 deletions(-)
 create mode 100644 dts/framework/params/testpmd.py

diff --git a/dts/framework/params/testpmd.py b/dts/framework/params/testpmd.py
new file mode 100644
index 0000000000..f8f70320cf
--- /dev/null
+++ b/dts/framework/params/testpmd.py
@@ -0,0 +1,608 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Module containing all the TestPmd-related parameter classes."""
+
+from dataclasses import dataclass, field
+from enum import EnumMeta, Flag, auto, unique
+from pathlib import PurePath
+from typing import Literal, NamedTuple
+
+from framework.params import (
+    Params,
+    Switch,
+    YesNoSwitch,
+    bracketed,
+    comma_separated,
+    convert_str,
+    hex_from_flag_value,
+    str_from_flag_value,
+)
+from framework.params.eal import EalParams
+from framework.utils import StrEnum
+
+
+class PortTopology(StrEnum):
+    """Enum representing the port topology."""
+
+    paired = auto()
+    """In paired mode, the forwarding is between pairs of ports, e.g.: (0,1), (2,3), (4,5)."""
+    chained = auto()
+    """In chained mode, the forwarding is to the next available port in the port mask, e.g.:
+    (0,1), (1,2), (2,0).
+
+    The ordering of the ports can be changed using the portlist testpmd runtime function.
+    """
+    loop = auto()
+    """In loop mode, ingress traffic is simply transmitted back on the same interface."""
+
+
+@convert_str(bracketed, comma_separated)
+class PortNUMAConfig(NamedTuple):
+    """DPDK port to NUMA socket association tuple."""
+
+    #:
+    port: int
+    #:
+    socket: int
+
+
+@convert_str(str_from_flag_value)
+@unique
+class FlowDirection(Flag):
+    """Flag indicating the direction of the flow.
+
+    A bi-directional flow can be specified with the pipe:
+
+    >>> TestPmdFlowDirection.RX | TestPmdFlowDirection.TX
+    <TestPmdFlowDirection.TX|RX: 3>
+    """
+
+    #:
+    RX = 1 << 0
+    #:
+    TX = 1 << 1
+
+
+@convert_str(bracketed, comma_separated)
+class RingNUMAConfig(NamedTuple):
+    """Tuple associating DPDK port, direction of the flow and NUMA socket."""
+
+    #:
+    port: int
+    #:
+    direction: FlowDirection
+    #:
+    socket: int
+
+
+@convert_str(comma_separated)
+class EthPeer(NamedTuple):
+    """Tuple associating a MAC address to the specified DPDK port."""
+
+    #:
+    port_no: int
+    #:
+    mac_address: str
+
+
+@convert_str(comma_separated)
+class TxIPAddrPair(NamedTuple):
+    """Tuple specifying the source and destination IPs for the packets."""
+
+    #:
+    source_ip: str
+    #:
+    dest_ip: str
+
+
+@convert_str(comma_separated)
+class TxUDPPortPair(NamedTuple):
+    """Tuple specifying the UDP source and destination ports for the packets.
+
+    If leaving ``dest_port`` unspecified, ``source_port`` will be used for
+    the destination port as well.
+    """
+
+    #:
+    source_port: int
+    #:
+    dest_port: int | None = None
+
+
+@dataclass
+class DisableRSS(Params):
+    """Disables RSS (Receive Side Scaling)."""
+
+    _disable_rss: Literal[True] = field(
+        default=True, init=False, metadata=Params.long("disable-rss")
+    )
+
+
+@dataclass
+class SetRSSIPOnly(Params):
+    """Sets RSS (Receive Side Scaling) functions for IPv4/IPv6 only."""
+
+    _rss_ip: Literal[True] = field(default=True, init=False, metadata=Params.long("rss-ip"))
+
+
+@dataclass
+class SetRSSUDP(Params):
+    """Sets RSS (Receive Side Scaling) functions for IPv4/IPv6 and UDP."""
+
+    _rss_udp: Literal[True] = field(default=True, init=False, metadata=Params.long("rss-udp"))
+
+
+class RSSSetting(EnumMeta):
+    """Enum representing a RSS setting. Each property is a class that needs to be initialised."""
+
+    #:
+    Disabled = DisableRSS
+    #:
+    SetIPOnly = SetRSSIPOnly
+    #:
+    SetUDP = SetRSSUDP
+
+
+class SimpleForwardingModes(StrEnum):
+    r"""The supported packet forwarding modes for :class:`~TestPmdShell`\s."""
+
+    #:
+    io = auto()
+    #:
+    mac = auto()
+    #:
+    macswap = auto()
+    #:
+    rxonly = auto()
+    #:
+    csum = auto()
+    #:
+    icmpecho = auto()
+    #:
+    ieee1588 = auto()
+    #:
+    fivetswap = "5tswap"
+    #:
+    shared_rxq = "shared-rxq"
+    #:
+    recycle_mbufs = auto()
+
+
+@dataclass(kw_only=True)
+class TXOnlyForwardingMode(Params):
+    """Sets a TX-Only forwarding mode.
+
+    Attributes:
+        multi_flow: Generates multiple flows if set to True.
+        segments_length: Sets TX segment sizes or total packet length.
+    """
+
+    _forward_mode: Literal["txonly"] = field(
+        default="txonly", init=False, metadata=Params.long("forward-mode")
+    )
+    multi_flow: Switch = field(default=None, metadata=Params.long("txonly-multi-flow"))
+    segments_length: list[int] | None = field(
+        default=None, metadata=Params.long("txpkts") | Params.convert_value(comma_separated)
+    )
+
+
+@dataclass(kw_only=True)
+class FlowGenForwardingMode(Params):
+    """Sets a flowgen forwarding mode.
+
+    Attributes:
+        clones: Set the number of each packet clones to be sent. Sending clones reduces host CPU
+                load on creating packets and may help in testing extreme speeds or maxing out
+                Tx packet performance. N should be not zero, but less than ‘burst’ parameter.
+        flows: Set the number of flows to be generated, where 1 <= N <= INT32_MAX.
+        segments_length: Set TX segment sizes or total packet length.
+    """
+
+    _forward_mode: Literal["flowgen"] = field(
+        default="flowgen", init=False, metadata=Params.long("forward-mode")
+    )
+    clones: int | None = field(default=None, metadata=Params.long("flowgen-clones"))
+    flows: int | None = field(default=None, metadata=Params.long("flowgen-flows"))
+    segments_length: list[int] | None = field(
+        default=None, metadata=Params.long("txpkts") | Params.convert_value(comma_separated)
+    )
+
+
+@dataclass(kw_only=True)
+class NoisyForwardingMode(Params):
+    """Sets a noisy forwarding mode.
+
+    Attributes:
+        forward_mode: Set the noisy VNF forwarding mode.
+        tx_sw_buffer_size: Set the maximum number of elements of the FIFO queue to be created for
+                           buffering packets.
+        tx_sw_buffer_flushtime: Set the time before packets in the FIFO queue are flushed.
+        lkup_memory: Set the size of the noisy neighbor simulation memory buffer in MB to N.
+        lkup_num_reads: Set the size of the noisy neighbor simulation memory buffer in MB to N.
+        lkup_num_writes: Set the number of writes to be done in noisy neighbor simulation
+                         memory buffer to N.
+        lkup_num_reads_writes: Set the number of r/w accesses to be done in noisy neighbor
+                               simulation memory buffer to N.
+    """
+
+    _forward_mode: Literal["noisy"] = field(
+        default="noisy", init=False, metadata=Params.long("forward-mode")
+    )
+    forward_mode: (
+        Literal[
+            SimpleForwardingModes.io,
+            SimpleForwardingModes.mac,
+            SimpleForwardingModes.macswap,
+            SimpleForwardingModes.fivetswap,
+        ]
+        | None
+    ) = field(default=SimpleForwardingModes.io, metadata=Params.long("noisy-forward-mode"))
+    tx_sw_buffer_size: int | None = field(
+        default=None, metadata=Params.long("noisy-tx-sw-buffer-size")
+    )
+    tx_sw_buffer_flushtime: int | None = field(
+        default=None, metadata=Params.long("noisy-tx-sw-buffer-flushtime")
+    )
+    lkup_memory: int | None = field(default=None, metadata=Params.long("noisy-lkup-memory"))
+    lkup_num_reads: int | None = field(default=None, metadata=Params.long("noisy-lkup-num-reads"))
+    lkup_num_writes: int | None = field(default=None, metadata=Params.long("noisy-lkup-num-writes"))
+    lkup_num_reads_writes: int | None = field(
+        default=None, metadata=Params.long("noisy-lkup-num-reads-writes")
+    )
+
+
+@convert_str(hex_from_flag_value)
+@unique
+class HairpinMode(Flag):
+    """Flag representing the hairpin mode."""
+
+    TWO_PORTS_LOOP = 1 << 0
+    """Two hairpin ports loop."""
+    TWO_PORTS_PAIRED = 1 << 1
+    """Two hairpin ports paired."""
+    EXPLICIT_TX_FLOW = 1 << 4
+    """Explicit Tx flow rule."""
+    FORCE_RX_QUEUE_MEM_SETTINGS = 1 << 8
+    """Force memory settings of hairpin RX queue."""
+    FORCE_TX_QUEUE_MEM_SETTINGS = 1 << 9
+    """Force memory settings of hairpin TX queue."""
+    RX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 12
+    """Hairpin RX queues will use locked device memory."""
+    RX_QUEUE_USE_RTE_MEMORY = 1 << 13
+    """Hairpin RX queues will use RTE memory."""
+    TX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 16
+    """Hairpin TX queues will use locked device memory."""
+    TX_QUEUE_USE_RTE_MEMORY = 1 << 18
+    """Hairpin TX queues will use RTE memory."""
+
+
+@dataclass(kw_only=True)
+class RXRingParams(Params):
+    """Sets the RX ring parameters.
+
+    Attributes:
+        descriptors: Set the number of descriptors in the RX rings to N, where N > 0.
+        prefetch_threshold: Set the prefetch threshold register of RX rings to N, where N >= 0.
+        host_threshold: Set the host threshold register of RX rings to N, where N >= 0.
+        write_back_threshold: Set the write-back threshold register of RX rings to N, where N >= 0.
+        free_threshold: Set the free threshold of RX descriptors to N,
+                        where 0 <= N < value of ``-–rxd``.
+    """
+
+    descriptors: int | None = field(default=None, metadata=Params.long("rxd"))
+    prefetch_threshold: int | None = field(default=None, metadata=Params.long("rxpt"))
+    host_threshold: int | None = field(default=None, metadata=Params.long("rxht"))
+    write_back_threshold: int | None = field(default=None, metadata=Params.long("rxwt"))
+    free_threshold: int | None = field(default=None, metadata=Params.long("rxfreet"))
+
+
+@convert_str(hex_from_flag_value)
+@unique
+class RXMultiQueueMode(Flag):
+    """Flag representing the RX multi-queue mode."""
+
+    #:
+    RSS = 1 << 0
+    #:
+    DCB = 1 << 1
+    #:
+    VMDQ = 1 << 2
+
+
+@dataclass(kw_only=True)
+class TXRingParams(Params):
+    """Sets the TX ring parameters.
+
+    Attributes:
+        descriptors: Set the number of descriptors in the TX rings to N, where N > 0.
+        rs_bit_threshold: Set the transmit RS bit threshold of TX rings to N,
+                          where 0 <= N <= value of ``--txd``.
+        prefetch_threshold: Set the prefetch threshold register of TX rings to N, where N >= 0.
+        host_threshold: Set the host threshold register of TX rings to N, where N >= 0.
+        write_back_threshold: Set the write-back threshold register of TX rings to N, where N >= 0.
+        free_threshold: Set the transmit free threshold of TX rings to N,
+                        where 0 <= N <= value of ``--txd``.
+    """
+
+    descriptors: int | None = field(default=None, metadata=Params.long("txd"))
+    rs_bit_threshold: int | None = field(default=None, metadata=Params.long("txrst"))
+    prefetch_threshold: int | None = field(default=None, metadata=Params.long("txpt"))
+    host_threshold: int | None = field(default=None, metadata=Params.long("txht"))
+    write_back_threshold: int | None = field(default=None, metadata=Params.long("txwt"))
+    free_threshold: int | None = field(default=None, metadata=Params.long("txfreet"))
+
+
+class Event(StrEnum):
+    """Enum representing a testpmd event."""
+
+    #:
+    unknown = auto()
+    #:
+    queue_state = auto()
+    #:
+    vf_mbox = auto()
+    #:
+    macsec = auto()
+    #:
+    intr_lsc = auto()
+    #:
+    intr_rmv = auto()
+    #:
+    intr_reset = auto()
+    #:
+    dev_probed = auto()
+    #:
+    dev_released = auto()
+    #:
+    flow_aged = auto()
+    #:
+    err_recovering = auto()
+    #:
+    recovery_success = auto()
+    #:
+    recovery_failed = auto()
+    #:
+    all = auto()
+
+
+class SimpleMempoolAllocationMode(StrEnum):
+    """Enum representing simple mempool allocation modes."""
+
+    native = auto()
+    """Create and populate mempool using native DPDK memory."""
+    xmem = auto()
+    """Create and populate mempool using externally and anonymously allocated area."""
+    xmemhuge = auto()
+    """Create and populate mempool using externally and anonymously allocated hugepage area."""
+
+
+@dataclass(kw_only=True)
+class AnonMempoolAllocationMode(Params):
+    """Create mempool using native DPDK memory, but populate using anonymous memory.
+
+    Attributes:
+        no_iova_contig: Enables to create mempool which is not IOVA contiguous.
+    """
+
+    _mp_alloc: Literal["anon"] = field(default="anon", init=False, metadata=Params.long("mp-alloc"))
+    no_iova_contig: Switch = None
+
+
+@dataclass(slots=True, kw_only=True)
+class TestPmdParams(EalParams):
+    """The testpmd shell parameters.
+
+    Attributes:
+        interactive_mode: Runs testpmd in interactive mode.
+        auto_start: Start forwarding on initialization.
+        tx_first: Start forwarding, after sending a burst of packets first.
+        stats_period: Display statistics every ``PERIOD`` seconds, if interactive mode is disabled.
+                      The default value is 0, which means that the statistics will not be displayed.
+
+                      .. note:: This flag should be used only in non-interactive mode.
+        display_xstats: Display comma-separated list of extended statistics every ``PERIOD`` seconds
+                        as specified in ``--stats-period`` or when used with interactive commands
+                        that show Rx/Tx statistics (i.e. ‘show port stats’).
+        nb_cores: Set the number of forwarding cores, where 1 <= N <= “number of cores” or
+                  ``RTE_MAX_LCORE`` from the configuration file.
+        coremask: Set the bitmask of the cores running the packet forwarding test. The main
+                  lcore is reserved for command line parsing only and cannot be masked on for packet
+                  forwarding.
+        nb_ports: Set the number of forwarding ports, where 1 <= N <= “number of ports” on the board
+                  or ``RTE_MAX_ETHPORTS`` from the configuration file. The default value is the
+                  number of ports on the board.
+        port_topology: Set port topology, where mode is paired (the default), chained or loop.
+        portmask: Set the bitmask of the ports used by the packet forwarding test.
+        portlist: Set the forwarding ports based on the user input used by the packet forwarding
+                  test. ‘-‘ denotes a range of ports to set including the two specified port IDs ‘,’
+                  separates multiple port values. Possible examples like –portlist=0,1 or
+                  –portlist=0-2 or –portlist=0,1-2 etc.
+        numa: Enable/disable NUMA-aware allocation of RX/TX rings and of RX memory buffers (mbufs).
+        socket_num: Set the socket from which all memory is allocated in NUMA mode, where
+                    0 <= N < number of sockets on the board.
+        port_numa_config: Specify the socket on which the memory pool to be used by the port will be
+                          allocated.
+        ring_numa_config: Specify the socket on which the TX/RX rings for the port will be
+                          allocated. Where flag is 1 for RX, 2 for TX, and 3 for RX and TX.
+        total_num_mbufs: Set the number of mbufs to be allocated in the mbuf pools, where N > 1024.
+        mbuf_size: Set the data size of the mbufs used to N bytes, where N < 65536.
+                   If multiple mbuf-size values are specified the extra memory pools will be created
+                   for allocating mbufs to receive packets with buffer splitting features.
+        mbcache: Set the cache of mbuf memory pools to N, where 0 <= N <= 512.
+        max_pkt_len: Set the maximum packet size to N bytes, where N >= 64.
+        eth_peers_configfile: Use a configuration file containing the Ethernet addresses of
+                              the peer ports.
+        eth_peer: Set the MAC address XX:XX:XX:XX:XX:XX of the peer port N,
+                  where 0 <= N < RTE_MAX_ETHPORTS.
+        tx_ip: Set the source and destination IP address used when doing transmit only test.
+               The defaults address values are source 198.18.0.1 and destination 198.18.0.2.
+               These are special purpose addresses reserved for benchmarking (RFC 5735).
+        tx_udp: Set the source and destination UDP port number for transmit test only test.
+                The default port is the port 9 which is defined for the discard protocol (RFC 863).
+        enable_lro: Enable large receive offload.
+        max_lro_pkt_size: Set the maximum LRO aggregated packet size to N bytes, where N >= 64.
+        disable_crc_strip: Disable hardware CRC stripping.
+        enable_scatter: Enable scatter (multi-segment) RX.
+        enable_hw_vlan: Enable hardware VLAN.
+        enable_hw_vlan_filter: Enable hardware VLAN filter.
+        enable_hw_vlan_strip: Enable hardware VLAN strip.
+        enable_hw_vlan_extend: Enable hardware VLAN extend.
+        enable_hw_qinq_strip: Enable hardware QINQ strip.
+        pkt_drop_enabled: Enable per-queue packet drop for packets with no descriptors.
+        rss: Receive Side Scaling setting.
+        forward_mode: Set the forwarding mode.
+        hairpin_mode: Set the hairpin port configuration.
+        hairpin_queues: Set the number of hairpin queues per port to N, where 1 <= N <= 65535.
+        burst: Set the number of packets per burst to N, where 1 <= N <= 512.
+        enable_rx_cksum: Enable hardware RX checksum offload.
+        rx_queues: Set the number of RX queues per port to N, where 1 <= N <= 65535.
+        rx_ring: Set the RX rings parameters.
+        no_flush_rx: Don’t flush the RX streams before starting forwarding. Used mainly with
+                     the PCAP PMD.
+        rx_segments_offsets: Set the offsets of packet segments on receiving
+                             if split feature is engaged.
+        rx_segments_length: Set the length of segments to scatter packets on receiving
+                            if split feature is engaged.
+        multi_rx_mempool: Enable multiple mbuf pools per Rx queue.
+        rx_shared_queue: Create queues in shared Rx queue mode if device supports. Shared Rx queues
+                         are grouped per X ports. X defaults to UINT32_MAX, implies all ports join
+                         share group 1. Forwarding engine “shared-rxq” should be used for shared Rx
+                         queues. This engine does Rx only and update stream statistics accordingly.
+        rx_offloads: Set the bitmask of RX queue offloads.
+        rx_mq_mode: Set the RX multi queue mode which can be enabled.
+        tx_queues: Set the number of TX queues per port to N, where 1 <= N <= 65535.
+        tx_ring: Set the TX rings params.
+        tx_offloads: Set the hexadecimal bitmask of TX queue offloads.
+        eth_link_speed: Set a forced link speed to the ethernet port. E.g. 1000 for 1Gbps.
+        disable_link_check: Disable check on link status when starting/stopping ports.
+        disable_device_start: Do not automatically start all ports. This allows testing
+                              configuration of rx and tx queues before device is started
+                              for the first time.
+        no_lsc_interrupt: Disable LSC interrupts for all ports, even those supporting it.
+        no_rmv_interrupt: Disable RMV interrupts for all ports, even those supporting it.
+        bitrate_stats: Set the logical core N to perform bitrate calculation.
+        latencystats: Set the logical core N to perform latency and jitter calculations.
+        print_events: Enable printing the occurrence of the designated events.
+                      Using :attr:`TestPmdEvent.ALL` will enable all of them.
+        mask_events: Disable printing the occurrence of the designated events.
+                     Using :attr:`TestPmdEvent.ALL` will disable all of them.
+        flow_isolate_all: Providing this parameter requests flow API isolated mode on all ports at
+                          initialization time. It ensures all traffic is received through the
+                          configured flow rules only (see flow command). Ports that do not support
+                          this mode are automatically discarded.
+        disable_flow_flush: Disable port flow flush when stopping port.
+                            This allows testing keep flow rules or shared flow objects across
+                            restart.
+        hot_plug: Enable device event monitor mechanism for hotplug.
+        vxlan_gpe_port: Set the UDP port number of tunnel VXLAN-GPE to N.
+        geneve_parsed_port: Set the UDP port number that is used for parsing the GENEVE protocol
+                            to N. HW may be configured with another tunnel Geneve port.
+        lock_all_memory: Enable/disable locking all memory. Disabled by default.
+        mempool_allocation_mode: Set mempool allocation mode.
+        record_core_cycles: Enable measurement of CPU cycles per packet.
+        record_burst_status: Enable display of RX and TX burst stats.
+    """
+
+    interactive_mode: Switch = field(default=True, metadata=Params.short("i"))
+    auto_start: Switch = field(default=None, metadata=Params.short("a"))
+    tx_first: Switch = None
+    stats_period: int | None = None
+    display_xstats: list[str] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    nb_cores: int | None = None
+    coremask: int | None = field(default=None, metadata=Params.convert_value(hex))
+    nb_ports: int | None = None
+    port_topology: PortTopology | None = PortTopology.paired
+    portmask: int | None = field(default=None, metadata=Params.convert_value(hex))
+    portlist: str | None = None  # TODO: can be ranges 0,1-3
+
+    numa: YesNoSwitch = True
+    socket_num: int | None = None
+    port_numa_config: list[PortNUMAConfig] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    ring_numa_config: list[RingNUMAConfig] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    total_num_mbufs: int | None = None
+    mbuf_size: list[int] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    mbcache: int | None = None
+    max_pkt_len: int | None = None
+    eth_peers_configfile: PurePath | None = None
+    eth_peer: list[EthPeer] | None = field(default=None, metadata=Params.multiple())
+    tx_ip: TxIPAddrPair | None = TxIPAddrPair(source_ip="198.18.0.1", dest_ip="198.18.0.2")
+    tx_udp: TxUDPPortPair | None = TxUDPPortPair(9)
+    enable_lro: Switch = None
+    max_lro_pkt_size: int | None = None
+    disable_crc_strip: Switch = None
+    enable_scatter: Switch = None
+    enable_hw_vlan: Switch = None
+    enable_hw_vlan_filter: Switch = None
+    enable_hw_vlan_strip: Switch = None
+    enable_hw_vlan_extend: Switch = None
+    enable_hw_qinq_strip: Switch = None
+    pkt_drop_enabled: Switch = field(default=None, metadata=Params.long("enable-drop-en"))
+    rss: RSSSetting | None = None
+    forward_mode: (
+        SimpleForwardingModes
+        | FlowGenForwardingMode
+        | TXOnlyForwardingMode
+        | NoisyForwardingMode
+        | None
+    ) = SimpleForwardingModes.io
+    hairpin_mode: HairpinMode | None = HairpinMode(0)
+    hairpin_queues: int | None = field(default=None, metadata=Params.long("hairpinq"))
+    burst: int | None = None
+    enable_rx_cksum: Switch = None
+
+    rx_queues: int | None = field(default=None, metadata=Params.long("rxq"))
+    rx_ring: RXRingParams | None = None
+    no_flush_rx: Switch = None
+    rx_segments_offsets: list[int] | None = field(
+        default=None, metadata=Params.long("rxoffs") | Params.convert_value(comma_separated)
+    )
+    rx_segments_length: list[int] | None = field(
+        default=None, metadata=Params.long("rxpkts") | Params.convert_value(comma_separated)
+    )
+    multi_rx_mempool: Switch = None
+    rx_shared_queue: Switch | int = field(default=None, metadata=Params.long("rxq-share"))
+    rx_offloads: int | None = field(default=None, metadata=Params.convert_value(hex))
+    rx_mq_mode: RXMultiQueueMode | None = (
+        RXMultiQueueMode.DCB | RXMultiQueueMode.RSS | RXMultiQueueMode.VMDQ
+    )
+
+    tx_queues: int | None = field(default=None, metadata=Params.long("txq"))
+    tx_ring: TXRingParams | None = None
+    tx_offloads: int | None = field(default=None, metadata=Params.convert_value(hex))
+
+    eth_link_speed: int | None = None
+    disable_link_check: Switch = None
+    disable_device_start: Switch = None
+    no_lsc_interrupt: Switch = None
+    no_rmv_interrupt: Switch = None
+    bitrate_stats: int | None = None
+    latencystats: int | None = None
+    print_events: list[Event] | None = field(
+        default=None, metadata=Params.multiple() | Params.long("print-event")
+    )
+    mask_events: list[Event] | None = field(
+        default_factory=lambda: [Event.intr_lsc],
+        metadata=Params.multiple() | Params.long("mask-event"),
+    )
+
+    flow_isolate_all: Switch = None
+    disable_flow_flush: Switch = None
+
+    hot_plug: Switch = None
+    vxlan_gpe_port: int | None = None
+    geneve_parsed_port: int | None = None
+    lock_all_memory: YesNoSwitch = field(default=False, metadata=Params.long("mlockall"))
+    mempool_allocation_mode: SimpleMempoolAllocationMode | AnonMempoolAllocationMode | None = field(
+        default=None, metadata=Params.long("mp-alloc")
+    )
+    record_core_cycles: Switch = None
+    record_burst_status: Switch = None
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index 841d456a2f..ef3f23c582 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright(c) 2023 University of New Hampshire
 # Copyright(c) 2023 PANTHEON.tech s.r.o.
+# Copyright(c) 2024 Arm Limited
 
 """Testpmd interactive shell.
 
@@ -16,14 +17,12 @@
 """
 
 import time
-from enum import auto
 from pathlib import PurePath
 from typing import Callable, ClassVar
 
 from framework.exception import InteractiveCommandExecutionError
-from framework.params.eal import EalParams
+from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
 from framework.settings import SETTINGS
-from framework.utils import StrEnum
 
 from .interactive_shell import InteractiveShell
 
@@ -50,37 +49,6 @@ def __str__(self) -> str:
         return self.pci_address
 
 
-class TestPmdForwardingModes(StrEnum):
-    r"""The supported packet forwarding modes for :class:`~TestPmdShell`\s."""
-
-    #:
-    io = auto()
-    #:
-    mac = auto()
-    #:
-    macswap = auto()
-    #:
-    flowgen = auto()
-    #:
-    rxonly = auto()
-    #:
-    txonly = auto()
-    #:
-    csum = auto()
-    #:
-    icmpecho = auto()
-    #:
-    ieee1588 = auto()
-    #:
-    noisy = auto()
-    #:
-    fivetswap = "5tswap"
-    #:
-    shared_rxq = "shared-rxq"
-    #:
-    recycle_mbufs = auto()
-
-
 class TestPmdShell(InteractiveShell):
     """Testpmd interactive shell.
 
@@ -119,9 +87,7 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
         Also find the number of pci addresses which were allowed on the command line when the app
         was started.
         """
-        self._app_params += " -i --mask-event intr_lsc"
-
-        assert isinstance(self._app_params, EalParams)
+        assert isinstance(self._app_params, TestPmdParams)
 
         self.number_of_ports = (
             len(self._app_params.ports) if self._app_params.ports is not None else 0
@@ -213,7 +179,7 @@ def wait_link_status_up(self, port_id: int, timeout=SETTINGS.timeout) -> bool:
             self._logger.error(f"The link for port {port_id} did not come up in the given timeout.")
         return "Link status: up" in port_info
 
-    def set_forward_mode(self, mode: TestPmdForwardingModes, verify: bool = True):
+    def set_forward_mode(self, mode: SimpleForwardingModes, verify: bool = True):
         """Set packet forwarding mode.
 
         Args:
diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index c6e93839cb..578b5a4318 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -23,7 +23,8 @@
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
 from framework.params import Params
-from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
+from framework.params.testpmd import SimpleForwardingModes
+from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.test_suite import TestSuite
 
 
@@ -113,7 +114,7 @@ def pmd_scatter(self, mbsize: int) -> None:
             ),
             privileged=True,
         )
-        testpmd.set_forward_mode(TestPmdForwardingModes.mac)
+        testpmd.set_forward_mode(SimpleForwardingModes.mac)
         testpmd.start()
 
         for offset in [-1, 0, 1, 4, 5]:
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v2 6/8] dts: use testpmd params for scatter test suite
  2024-05-09 11:20 ` [PATCH v2 0/8] dts: add testpmd params Luca Vizzarro
                     ` (4 preceding siblings ...)
  2024-05-09 11:20   ` [PATCH v2 5/8] dts: add testpmd shell params Luca Vizzarro
@ 2024-05-09 11:20   ` Luca Vizzarro
  2024-05-28 15:49     ` Nicholas Pratte
  2024-05-09 11:20   ` [PATCH v2 7/8] dts: rework interactive shells Luca Vizzarro
                     ` (2 subsequent siblings)
  8 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-05-09 11:20 UTC (permalink / raw)
  To: dev; +Cc: Juraj Linkeš, Jeremy Spewock, Luca Vizzarro, Paul Szczepanek

Update the buffer scatter test suite to use TestPmdParameters
instead of the StrParams implementation.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 dts/tests/TestSuite_pmd_buffer_scatter.py | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index 578b5a4318..6d206c1a40 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -16,14 +16,14 @@
 """
 
 import struct
+from dataclasses import asdict
 
 from scapy.layers.inet import IP  # type: ignore[import-untyped]
 from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
 from scapy.packet import Raw  # type: ignore[import-untyped]
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
-from framework.params import Params
-from framework.params.testpmd import SimpleForwardingModes
+from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
 from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.test_suite import TestSuite
 
@@ -105,16 +105,16 @@ def pmd_scatter(self, mbsize: int) -> None:
         """
         testpmd = self.sut_node.create_interactive_shell(
             TestPmdShell,
-            app_params=Params.from_str(
-                "--mbcache=200 "
-                f"--mbuf-size={mbsize} "
-                "--max-pkt-len=9000 "
-                "--port-topology=paired "
-                "--tx-offloads=0x00008000"
+            app_params=TestPmdParams(
+                forward_mode=SimpleForwardingModes.mac,
+                mbcache=200,
+                mbuf_size=[mbsize],
+                max_pkt_len=9000,
+                tx_offloads=0x00008000,
+                **asdict(self.sut_node.create_eal_parameters()),
             ),
             privileged=True,
         )
-        testpmd.set_forward_mode(SimpleForwardingModes.mac)
         testpmd.start()
 
         for offset in [-1, 0, 1, 4, 5]:
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v2 7/8] dts: rework interactive shells
  2024-05-09 11:20 ` [PATCH v2 0/8] dts: add testpmd params Luca Vizzarro
                     ` (5 preceding siblings ...)
  2024-05-09 11:20   ` [PATCH v2 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
@ 2024-05-09 11:20   ` Luca Vizzarro
  2024-05-28 15:50     ` Nicholas Pratte
  2024-05-28 21:07     ` Jeremy Spewock
  2024-05-09 11:20   ` [PATCH v2 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
  2024-05-22 15:59   ` [PATCH v2 0/8] dts: add testpmd params Nicholas Pratte
  8 siblings, 2 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-05-09 11:20 UTC (permalink / raw)
  To: dev; +Cc: Juraj Linkeš, Jeremy Spewock, Luca Vizzarro, Paul Szczepanek

The way nodes and interactive shells interact makes it difficult to
develop for static type checking and hinting. The current system relies
on a top-down approach, attempting to give a generic interface to the
test developer, hiding the interaction of concrete shell classes as much
as possible. When working with strong typing this approach is not ideal,
as Python's implementation of generics is still rudimentary.

This rework reverses the tests interaction to a bottom-up approach,
allowing the test developer to call concrete shell classes directly,
and let them ingest nodes independently. While also re-enforcing type
checking and making the code easier to read.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 dts/framework/params/eal.py                   |   6 +-
 dts/framework/remote_session/dpdk_shell.py    | 104 ++++++++++++++++
 .../remote_session/interactive_shell.py       |  75 +++++++-----
 dts/framework/remote_session/python_shell.py  |   4 +-
 dts/framework/remote_session/testpmd_shell.py |  64 +++++-----
 dts/framework/testbed_model/node.py           |  36 +-----
 dts/framework/testbed_model/os_session.py     |  36 +-----
 dts/framework/testbed_model/sut_node.py       | 112 +-----------------
 .../testbed_model/traffic_generator/scapy.py  |   4 +-
 dts/tests/TestSuite_hello_world.py            |   7 +-
 dts/tests/TestSuite_pmd_buffer_scatter.py     |  21 ++--
 dts/tests/TestSuite_smoke_tests.py            |   2 +-
 12 files changed, 201 insertions(+), 270 deletions(-)
 create mode 100644 dts/framework/remote_session/dpdk_shell.py

diff --git a/dts/framework/params/eal.py b/dts/framework/params/eal.py
index bbdbc8f334..8d7766fefc 100644
--- a/dts/framework/params/eal.py
+++ b/dts/framework/params/eal.py
@@ -35,9 +35,9 @@ class EalParams(Params):
                 ``other_eal_param='--single-file-segments'``
     """
 
-    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
-    memory_channels: int = field(metadata=Params.short("n"))
-    prefix: str = field(metadata=Params.long("file-prefix"))
+    lcore_list: LogicalCoreList | None = field(default=None, metadata=Params.short("l"))
+    memory_channels: int | None = field(default=None, metadata=Params.short("n"))
+    prefix: str = field(default="dpdk", metadata=Params.long("file-prefix"))
     no_pci: Switch = None
     vdevs: list[VirtualDevice] | None = field(
         default=None, metadata=Params.multiple() | Params.long("vdev")
diff --git a/dts/framework/remote_session/dpdk_shell.py b/dts/framework/remote_session/dpdk_shell.py
new file mode 100644
index 0000000000..78caae36ea
--- /dev/null
+++ b/dts/framework/remote_session/dpdk_shell.py
@@ -0,0 +1,104 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""DPDK-based interactive shell.
+
+Provides a base class to create interactive shells based on DPDK.
+"""
+
+
+from abc import ABC
+
+from framework.params.eal import EalParams
+from framework.remote_session.interactive_shell import InteractiveShell
+from framework.settings import SETTINGS
+from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
+from framework.testbed_model.sut_node import SutNode
+
+
+def compute_eal_params(
+    node: SutNode,
+    params: EalParams | None = None,
+    lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
+    ascending_cores: bool = True,
+    append_prefix_timestamp: bool = True,
+) -> EalParams:
+    """Compute EAL parameters based on the node's specifications.
+
+    Args:
+        node: The SUT node to compute the values for.
+        params: The EalParams object to amend, if set to None a new object is created and returned.
+        lcore_filter_specifier: A number of lcores/cores/sockets to use
+            or a list of lcore ids to use.
+            The default will select one lcore for each of two cores
+            on one socket, in ascending order of core ids.
+        ascending_cores: Sort cores in ascending order (lowest to highest IDs).
+            If :data:`False`, sort in descending order.
+        append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.
+    """
+    if params is None:
+        params = EalParams()
+
+    if params.lcore_list is None:
+        params.lcore_list = LogicalCoreList(
+            node.filter_lcores(lcore_filter_specifier, ascending_cores)
+        )
+
+    prefix = params.prefix
+    if append_prefix_timestamp:
+        prefix = f"{prefix}_{node._dpdk_timestamp}"
+    prefix = node.main_session.get_dpdk_file_prefix(prefix)
+    if prefix:
+        node._dpdk_prefix_list.append(prefix)
+    params.prefix = prefix
+
+    if params.ports is None:
+        params.ports = node.ports
+
+    return params
+
+
+class DPDKShell(InteractiveShell, ABC):
+    """The base class for managing DPDK-based interactive shells.
+
+    This class shouldn't be instantiated directly, but instead be extended.
+    It automatically injects computed EAL parameters based on the node in the
+    supplied app parameters.
+    """
+
+    _node: SutNode
+    _app_params: EalParams
+
+    _lcore_filter_specifier: LogicalCoreCount | LogicalCoreList
+    _ascending_cores: bool
+    _append_prefix_timestamp: bool
+
+    def __init__(
+        self,
+        node: SutNode,
+        app_params: EalParams,
+        privileged: bool = True,
+        timeout: float = SETTINGS.timeout,
+        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
+        ascending_cores: bool = True,
+        append_prefix_timestamp: bool = True,
+        start_on_init: bool = True,
+    ) -> None:
+        """Overrides :meth:`~.interactive_shell.InteractiveShell.__init__`."""
+        self._lcore_filter_specifier = lcore_filter_specifier
+        self._ascending_cores = ascending_cores
+        self._append_prefix_timestamp = append_prefix_timestamp
+
+        super().__init__(node, app_params, privileged, timeout, start_on_init)
+
+    def __post_init__(self):
+        """Computes EAL params based on the node capabilities before start."""
+        self._app_params = compute_eal_params(
+            self._node,
+            self._app_params,
+            self._lcore_filter_specifier,
+            self._ascending_cores,
+            self._append_prefix_timestamp,
+        )
+
+        self._update_path(self._node.remote_dpdk_build_dir.joinpath(self.path))
diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
index 9da66d1c7e..8163c8f247 100644
--- a/dts/framework/remote_session/interactive_shell.py
+++ b/dts/framework/remote_session/interactive_shell.py
@@ -17,13 +17,14 @@
 
 from abc import ABC
 from pathlib import PurePath
-from typing import Callable, ClassVar
+from typing import ClassVar
 
-from paramiko import Channel, SSHClient, channel  # type: ignore[import-untyped]
+from paramiko import Channel, channel  # type: ignore[import-untyped]
 
 from framework.logger import DTSLogger
 from framework.params import Params
 from framework.settings import SETTINGS
+from framework.testbed_model.node import Node
 
 
 class InteractiveShell(ABC):
@@ -36,13 +37,14 @@ class InteractiveShell(ABC):
     session.
     """
 
-    _interactive_session: SSHClient
+    _node: Node
     _stdin: channel.ChannelStdinFile
     _stdout: channel.ChannelFile
     _ssh_channel: Channel
     _logger: DTSLogger
     _timeout: float
     _app_params: Params
+    _privileged: bool
 
     #: Prompt to expect at the end of output when sending a command.
     #: This is often overridden by subclasses.
@@ -56,57 +58,66 @@ class InteractiveShell(ABC):
     #: Path to the executable to start the interactive application.
     path: ClassVar[PurePath]
 
-    #: Whether this application is a DPDK app. If it is, the build directory
-    #: for DPDK on the node will be prepended to the path to the executable.
-    dpdk_app: ClassVar[bool] = False
-
     def __init__(
         self,
-        interactive_session: SSHClient,
-        logger: DTSLogger,
-        get_privileged_command: Callable[[str], str] | None,
+        node: Node,
         app_params: Params = Params(),
+        privileged: bool = False,
         timeout: float = SETTINGS.timeout,
+        start_on_init: bool = True,
     ) -> None:
         """Create an SSH channel during initialization.
 
         Args:
-            interactive_session: The SSH session dedicated to interactive shells.
-            logger: The logger instance this session will use.
-            get_privileged_command: A method for modifying a command to allow it to use
-                elevated privileges. If :data:`None`, the application will not be started
-                with elevated privileges.
+            node: The node on which to run start the interactive shell.
             app_params: The command line parameters to be passed to the application on startup.
+            privileged: Enables the shell to run as superuser.
             timeout: The timeout used for the SSH channel that is dedicated to this interactive
                 shell. This timeout is for collecting output, so if reading from the buffer
                 and no output is gathered within the timeout, an exception is thrown.
+            start_on_init: Start interactive shell automatically after object initialisation.
         """
-        self._interactive_session = interactive_session
-        self._ssh_channel = self._interactive_session.invoke_shell()
+        self._node = node
+        self._logger = node._logger
+        self._app_params = app_params
+        self._privileged = privileged
+        self._timeout = timeout
+        # Ensure path is properly formatted for the host
+        self._update_path(self._node.main_session.join_remote_path(self.path))
+
+        self.__post_init__()
+
+        if start_on_init:
+            self.start_application()
+
+    def __post_init__(self):
+        """Overridable. Method called after the object init and before application start."""
+        pass
+
+    def _setup_ssh_channel(self):
+        self._ssh_channel = self._node.main_session.interactive_session.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.settimeout(self._timeout)
         self._ssh_channel.set_combine_stderr(True)  # combines stdout and stderr streams
-        self._logger = logger
-        self._timeout = timeout
-        self._app_params = app_params
-        self._start_application(get_privileged_command)
 
-    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
+    def start_application(self) -> None:
         """Starts a new interactive application based on the path to the app.
 
         This method is often overridden by subclasses as their process for
         starting may look different.
-
-        Args:
-            get_privileged_command: A function (but could be any callable) that produces
-                the version of the command with elevated privileges.
         """
-        start_command = f"{self.path} {self._app_params}"
-        if get_privileged_command is not None:
-            start_command = get_privileged_command(start_command)
+        self._setup_ssh_channel()
+
+        start_command = self._make_start_command()
+        if self._privileged:
+            start_command = self._node.main_session._get_privileged_command(start_command)
         self.send_command(start_command)
 
+    def _make_start_command(self) -> str:
+        """Makes the command that starts the interactive shell."""
+        return f"{self.path} {self._app_params or ''}"
+
     def send_command(self, command: str, prompt: str | None = None) -> str:
         """Send `command` and get all output before the expected ending string.
 
@@ -150,3 +161,7 @@ def close(self) -> None:
     def __del__(self) -> None:
         """Make sure the session is properly closed before deleting the object."""
         self.close()
+
+    @classmethod
+    def _update_path(cls, path: PurePath) -> None:
+        cls.path = path
diff --git a/dts/framework/remote_session/python_shell.py b/dts/framework/remote_session/python_shell.py
index ccfd3783e8..953ed100df 100644
--- a/dts/framework/remote_session/python_shell.py
+++ b/dts/framework/remote_session/python_shell.py
@@ -6,9 +6,7 @@
 Typical usage example in a TestSuite::
 
     from framework.remote_session import PythonShell
-    python_shell = self.tg_node.create_interactive_shell(
-        PythonShell, timeout=5, privileged=True
-    )
+    python_shell = PythonShell(self.tg_node, timeout=5, privileged=True)
     python_shell.send_command("print('Hello World')")
     python_shell.close()
 """
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index ef3f23c582..92930d7fbb 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -7,9 +7,7 @@
 
 Typical usage example in a TestSuite::
 
-    testpmd_shell = self.sut_node.create_interactive_shell(
-            TestPmdShell, privileged=True
-        )
+    testpmd_shell = TestPmdShell()
     devices = testpmd_shell.get_devices()
     for device in devices:
         print(device)
@@ -18,13 +16,14 @@
 
 import time
 from pathlib import PurePath
-from typing import Callable, ClassVar
+from typing import ClassVar
 
 from framework.exception import InteractiveCommandExecutionError
 from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
+from framework.remote_session.dpdk_shell import DPDKShell
 from framework.settings import SETTINGS
-
-from .interactive_shell import InteractiveShell
+from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
+from framework.testbed_model.sut_node import SutNode
 
 
 class TestPmdDevice(object):
@@ -49,52 +48,48 @@ def __str__(self) -> str:
         return self.pci_address
 
 
-class TestPmdShell(InteractiveShell):
+class TestPmdShell(DPDKShell):
     """Testpmd interactive shell.
 
     The testpmd shell users should never use
     the :meth:`~.interactive_shell.InteractiveShell.send_command` method directly, but rather
     call specialized methods. If there isn't one that satisfies a need, it should be added.
-
-    Attributes:
-        number_of_ports: The number of ports which were allowed on the command-line when testpmd
-            was started.
     """
 
-    number_of_ports: int
+    _app_params: TestPmdParams
 
     #: The path to the testpmd executable.
     path: ClassVar[PurePath] = PurePath("app", "dpdk-testpmd")
 
-    #: Flag this as a DPDK app so that it's clear this is not a system app and
-    #: needs to be looked in a specific path.
-    dpdk_app: ClassVar[bool] = True
-
     #: The testpmd's prompt.
     _default_prompt: ClassVar[str] = "testpmd>"
 
     #: This forces the prompt to appear after sending a command.
     _command_extra_chars: ClassVar[str] = "\n"
 
-    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
-        """Overrides :meth:`~.interactive_shell._start_application`.
-
-        Add flags for starting testpmd in interactive mode and disabling messages for link state
-        change events before starting the application. Link state is verified before starting
-        packet forwarding and the messages create unexpected newlines in the terminal which
-        complicates output collection.
-
-        Also find the number of pci addresses which were allowed on the command line when the app
-        was started.
-        """
-        assert isinstance(self._app_params, TestPmdParams)
-
-        self.number_of_ports = (
-            len(self._app_params.ports) if self._app_params.ports is not None else 0
+    def __init__(
+        self,
+        node: SutNode,
+        privileged: bool = True,
+        timeout: float = SETTINGS.timeout,
+        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
+        ascending_cores: bool = True,
+        append_prefix_timestamp: bool = True,
+        start_on_init: bool = True,
+        **app_params,
+    ) -> None:
+        """Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs."""
+        super().__init__(
+            node,
+            TestPmdParams(**app_params),
+            privileged,
+            timeout,
+            lcore_filter_specifier,
+            ascending_cores,
+            append_prefix_timestamp,
+            start_on_init,
         )
 
-        super()._start_application(get_privileged_command)
-
     def start(self, verify: bool = True) -> None:
         """Start packet forwarding with the current configuration.
 
@@ -114,7 +109,8 @@ def start(self, verify: bool = True) -> None:
                 self._logger.debug(f"Failed to start packet forwarding: \n{start_cmd_output}")
                 raise InteractiveCommandExecutionError("Testpmd failed to start packet forwarding.")
 
-            for port_id in range(self.number_of_ports):
+            number_of_ports = len(self._app_params.ports or [])
+            for port_id in range(number_of_ports):
                 if not self.wait_link_status_up(port_id):
                     raise InteractiveCommandExecutionError(
                         "Not all ports came up after starting packet forwarding in testpmd."
diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
index 6af4f25a3c..88395faabe 100644
--- a/dts/framework/testbed_model/node.py
+++ b/dts/framework/testbed_model/node.py
@@ -15,7 +15,7 @@
 
 from abc import ABC
 from ipaddress import IPv4Interface, IPv6Interface
-from typing import Any, Callable, Type, Union
+from typing import Any, Callable, Union
 
 from framework.config import (
     OS,
@@ -25,7 +25,6 @@
 )
 from framework.exception import ConfigurationError
 from framework.logger import DTSLogger, get_dts_logger
-from framework.params import Params
 from framework.settings import SETTINGS
 
 from .cpu import (
@@ -36,7 +35,7 @@
     lcore_filter,
 )
 from .linux_session import LinuxSession
-from .os_session import InteractiveShellType, OSSession
+from .os_session import OSSession
 from .port import Port
 from .virtual_device import VirtualDevice
 
@@ -196,37 +195,6 @@ def create_session(self, name: str) -> OSSession:
         self._other_sessions.append(connection)
         return connection
 
-    def create_interactive_shell(
-        self,
-        shell_cls: Type[InteractiveShellType],
-        timeout: float = SETTINGS.timeout,
-        privileged: bool = False,
-        app_params: Params = Params(),
-    ) -> InteractiveShellType:
-        """Factory for interactive session handlers.
-
-        Instantiate `shell_cls` according to the remote OS specifics.
-
-        Args:
-            shell_cls: The class of the shell.
-            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.
-            privileged: Whether to run the shell with administrative privileges.
-            app_args: The arguments to be passed to the application.
-
-        Returns:
-            An instance of the desired interactive application shell.
-        """
-        if not shell_cls.dpdk_app:
-            shell_cls.path = self.main_session.join_remote_path(shell_cls.path)
-
-        return self.main_session.create_interactive_shell(
-            shell_cls,
-            timeout,
-            privileged,
-            app_params,
-        )
-
     def filter_lcores(
         self,
         filter_specifier: LogicalCoreCount | LogicalCoreList,
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
index e5f5fcbe0e..e7e6c9d670 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -26,18 +26,16 @@
 from collections.abc import Iterable
 from ipaddress import IPv4Interface, IPv6Interface
 from pathlib import PurePath
-from typing import Type, TypeVar, Union
+from typing import Union
 
 from framework.config import Architecture, NodeConfiguration, NodeInfo
 from framework.logger import DTSLogger
-from framework.params import Params
 from framework.remote_session import (
     InteractiveRemoteSession,
     RemoteSession,
     create_interactive_session,
     create_remote_session,
 )
-from framework.remote_session.interactive_shell import InteractiveShell
 from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
@@ -45,8 +43,6 @@
 from .cpu import LogicalCore
 from .port import Port
 
-InteractiveShellType = TypeVar("InteractiveShellType", bound=InteractiveShell)
-
 
 class OSSession(ABC):
     """OS-unaware to OS-aware translation API definition.
@@ -131,36 +127,6 @@ def send_command(
 
         return self.remote_session.send_command(command, timeout, verify, env)
 
-    def create_interactive_shell(
-        self,
-        shell_cls: Type[InteractiveShellType],
-        timeout: float,
-        privileged: bool,
-        app_args: Params,
-    ) -> InteractiveShellType:
-        """Factory for interactive session handlers.
-
-        Instantiate `shell_cls` according to the remote OS specifics.
-
-        Args:
-            shell_cls: The class of the shell.
-            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.
-            privileged: Whether to run the shell with administrative privileges.
-            app_args: The arguments to be passed to the application.
-
-        Returns:
-            An instance of the desired interactive application shell.
-        """
-        return shell_cls(
-            self.interactive_session.session,
-            self._logger,
-            self._get_privileged_command if privileged else None,
-            app_args,
-            timeout,
-        )
-
     @staticmethod
     @abstractmethod
     def _get_privileged_command(command: str) -> str:
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index 83ad06ae2d..727170b7fc 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -16,7 +16,6 @@
 import tarfile
 import time
 from pathlib import PurePath
-from typing import Type
 
 from framework.config import (
     BuildTargetConfiguration,
@@ -24,17 +23,13 @@
     NodeInfo,
     SutNodeConfiguration,
 )
-from framework.params import Params, Switch
 from framework.params.eal import EalParams
 from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
 
-from .cpu import LogicalCoreCount, LogicalCoreList
 from .node import Node
-from .os_session import InteractiveShellType, OSSession
-from .port import Port
-from .virtual_device import VirtualDevice
+from .os_session import OSSession
 
 
 class SutNode(Node):
@@ -289,68 +284,6 @@ def kill_cleanup_dpdk_apps(self) -> None:
             self._dpdk_kill_session = self.create_session("dpdk_kill")
         self._dpdk_prefix_list = []
 
-    def create_eal_parameters(
-        self,
-        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
-        ascending_cores: bool = True,
-        prefix: str = "dpdk",
-        append_prefix_timestamp: bool = True,
-        no_pci: Switch = None,
-        vdevs: list[VirtualDevice] | None = None,
-        ports: list[Port] | None = None,
-        other_eal_param: str = "",
-    ) -> EalParams:
-        """Compose the EAL parameters.
-
-        Process the list of cores and the DPDK prefix and pass that along with
-        the rest of the arguments.
-
-        Args:
-            lcore_filter_specifier: A number of lcores/cores/sockets to use
-                or a list of lcore ids to use.
-                The default will select one lcore for each of two cores
-                on one socket, in ascending order of core ids.
-            ascending_cores: Sort cores in ascending order (lowest to highest IDs).
-                If :data:`False`, sort in descending order.
-            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
-            append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.
-            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
-            vdevs: Virtual devices, e.g.::
-
-                vdevs=[
-                    VirtualDevice('net_ring0'),
-                    VirtualDevice('net_ring1')
-                ]
-            ports: The list of ports to allow. If :data:`None`, all ports listed in `self.ports`
-                will be allowed.
-            other_eal_param: user defined DPDK EAL parameters, e.g.:
-                ``other_eal_param='--single-file-segments'``.
-
-        Returns:
-            An EAL param string, such as
-            ``-c 0xf -a 0000:88:00.0 --file-prefix=dpdk_1112_20190809143420``.
-        """
-        lcore_list = LogicalCoreList(self.filter_lcores(lcore_filter_specifier, ascending_cores))
-
-        if append_prefix_timestamp:
-            prefix = f"{prefix}_{self._dpdk_timestamp}"
-        prefix = self.main_session.get_dpdk_file_prefix(prefix)
-        if prefix:
-            self._dpdk_prefix_list.append(prefix)
-
-        if ports is None:
-            ports = self.ports
-
-        return EalParams(
-            lcore_list=lcore_list,
-            memory_channels=self.config.memory_channels,
-            prefix=prefix,
-            no_pci=no_pci,
-            vdevs=vdevs,
-            ports=ports,
-            other_eal_param=Params.from_str(other_eal_param),
-        )
-
     def run_dpdk_app(
         self, app_path: PurePath, eal_params: EalParams, timeout: float = 30
     ) -> CommandResult:
@@ -379,49 +312,6 @@ def configure_ipv4_forwarding(self, enable: bool) -> None:
         """
         self.main_session.configure_ipv4_forwarding(enable)
 
-    def create_interactive_shell(
-        self,
-        shell_cls: Type[InteractiveShellType],
-        timeout: float = SETTINGS.timeout,
-        privileged: bool = False,
-        app_params: Params = Params(),
-        eal_params: EalParams | None = None,
-    ) -> InteractiveShellType:
-        """Extend the factory for interactive session handlers.
-
-        The extensions are SUT node specific:
-
-            * The default for `eal_parameters`,
-            * The interactive shell path `shell_cls.path` is prepended with path to the remote
-              DPDK build directory for DPDK apps.
-
-        Args:
-            shell_cls: The class of the shell.
-            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.
-            privileged: Whether to run the shell with administrative privileges.
-            app_params: The parameters to be passed to the application.
-            eal_params: List of EAL parameters to use to launch the app. If this
-                isn't provided or an empty string is passed, it will default to calling
-                :meth:`create_eal_parameters`.
-
-        Returns:
-            An instance of the desired interactive application shell.
-        """
-        # We need to append the build directory and add EAL parameters for DPDK apps
-        if shell_cls.dpdk_app:
-            if eal_params is None:
-                eal_params = self.create_eal_parameters()
-            eal_params.append_str(str(app_params))
-            app_params = eal_params
-
-            shell_cls.path = self.main_session.join_remote_path(
-                self.remote_dpdk_build_dir, shell_cls.path
-            )
-
-        return super().create_interactive_shell(shell_cls, timeout, privileged, app_params)
-
     def bind_ports_to_driver(self, for_dpdk: bool = True) -> None:
         """Bind all ports on the SUT to a driver.
 
diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py
index 7bc1c2cc08..bf58ad1c5e 100644
--- a/dts/framework/testbed_model/traffic_generator/scapy.py
+++ b/dts/framework/testbed_model/traffic_generator/scapy.py
@@ -217,9 +217,7 @@ def __init__(self, tg_node: Node, config: ScapyTrafficGeneratorConfig):
             self._tg_node.config.os == OS.linux
         ), "Linux is the only supported OS for scapy traffic generation"
 
-        self.session = self._tg_node.create_interactive_shell(
-            PythonShell, timeout=5, privileged=True
-        )
+        self.session = PythonShell(self._tg_node, timeout=5, privileged=True)
 
         # import libs in remote python console
         for import_statement in SCAPY_RPC_SERVER_IMPORTS:
diff --git a/dts/tests/TestSuite_hello_world.py b/dts/tests/TestSuite_hello_world.py
index 0d6995f260..d958f99030 100644
--- a/dts/tests/TestSuite_hello_world.py
+++ b/dts/tests/TestSuite_hello_world.py
@@ -7,6 +7,7 @@
 No other EAL parameters apart from cores are used.
 """
 
+from framework.remote_session.dpdk_shell import compute_eal_params
 from framework.test_suite import TestSuite
 from framework.testbed_model.cpu import (
     LogicalCoreCount,
@@ -38,7 +39,7 @@ def test_hello_world_single_core(self) -> None:
         # get the first usable core
         lcore_amount = LogicalCoreCount(1, 1, 1)
         lcores = LogicalCoreCountFilter(self.sut_node.lcores, lcore_amount).filter()
-        eal_para = self.sut_node.create_eal_parameters(lcore_filter_specifier=lcore_amount)
+        eal_para = compute_eal_params(self.sut_node, lcore_filter_specifier=lcore_amount)
         result = self.sut_node.run_dpdk_app(self.app_helloworld_path, eal_para)
         self.verify(
             f"hello from core {int(lcores[0])}" in result.stdout,
@@ -55,8 +56,8 @@ def test_hello_world_all_cores(self) -> None:
             "hello from core <core_id>"
         """
         # get the maximum logical core number
-        eal_para = self.sut_node.create_eal_parameters(
-            lcore_filter_specifier=LogicalCoreList(self.sut_node.lcores)
+        eal_para = compute_eal_params(
+            self.sut_node, lcore_filter_specifier=LogicalCoreList(self.sut_node.lcores)
         )
         result = self.sut_node.run_dpdk_app(self.app_helloworld_path, eal_para, 50)
         for lcore in self.sut_node.lcores:
diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index 6d206c1a40..43cf5c61eb 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -16,14 +16,13 @@
 """
 
 import struct
-from dataclasses import asdict
 
 from scapy.layers.inet import IP  # type: ignore[import-untyped]
 from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
 from scapy.packet import Raw  # type: ignore[import-untyped]
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
-from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
+from framework.params.testpmd import SimpleForwardingModes
 from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.test_suite import TestSuite
 
@@ -103,17 +102,13 @@ def pmd_scatter(self, mbsize: int) -> None:
         Test:
             Start testpmd and run functional test with preset mbsize.
         """
-        testpmd = self.sut_node.create_interactive_shell(
-            TestPmdShell,
-            app_params=TestPmdParams(
-                forward_mode=SimpleForwardingModes.mac,
-                mbcache=200,
-                mbuf_size=[mbsize],
-                max_pkt_len=9000,
-                tx_offloads=0x00008000,
-                **asdict(self.sut_node.create_eal_parameters()),
-            ),
-            privileged=True,
+        testpmd = TestPmdShell(
+            self.sut_node,
+            forward_mode=SimpleForwardingModes.mac,
+            mbcache=200,
+            mbuf_size=[mbsize],
+            max_pkt_len=9000,
+            tx_offloads=0x00008000,
         )
         testpmd.start()
 
diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py
index ca678f662d..eca27acfd8 100644
--- a/dts/tests/TestSuite_smoke_tests.py
+++ b/dts/tests/TestSuite_smoke_tests.py
@@ -99,7 +99,7 @@ def test_devices_listed_in_testpmd(self) -> None:
         Test:
             List all devices found in testpmd and verify the configured devices are among them.
         """
-        testpmd_driver = self.sut_node.create_interactive_shell(TestPmdShell, privileged=True)
+        testpmd_driver = TestPmdShell(self.sut_node)
         dev_list = [str(x) for x in testpmd_driver.get_devices()]
         for nic in self.nics_in_node:
             self.verify(
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v2 8/8] dts: use Unpack for type checking and hinting
  2024-05-09 11:20 ` [PATCH v2 0/8] dts: add testpmd params Luca Vizzarro
                     ` (6 preceding siblings ...)
  2024-05-09 11:20   ` [PATCH v2 7/8] dts: rework interactive shells Luca Vizzarro
@ 2024-05-09 11:20   ` Luca Vizzarro
  2024-05-28 15:50     ` Nicholas Pratte
  2024-05-28 21:08     ` Jeremy Spewock
  2024-05-22 15:59   ` [PATCH v2 0/8] dts: add testpmd params Nicholas Pratte
  8 siblings, 2 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-05-09 11:20 UTC (permalink / raw)
  To: dev; +Cc: Juraj Linkeš, Jeremy Spewock, Luca Vizzarro, Paul Szczepanek

Interactive shells that inherit DPDKShell initialise their params
classes from a kwargs dict. Therefore, static type checking is
disabled. This change uses the functionality of Unpack added in
PEP 692 to re-enable it. The disadvantage is that this functionality has
been implemented only with TypedDict, forcing the creation of TypedDict
mirrors of the Params classes.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 dts/framework/params/types.py                 | 133 ++++++++++++++++++
 dts/framework/remote_session/testpmd_shell.py |   5 +-
 2 files changed, 137 insertions(+), 1 deletion(-)
 create mode 100644 dts/framework/params/types.py

diff --git a/dts/framework/params/types.py b/dts/framework/params/types.py
new file mode 100644
index 0000000000..e668f658d8
--- /dev/null
+++ b/dts/framework/params/types.py
@@ -0,0 +1,133 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Module containing TypeDict-equivalents of Params classes for static typing and hinting.
+
+TypedDicts can be used in conjunction with Unpack and kwargs for type hinting on function calls.
+
+Example:
+    ..code:: python
+        def create_testpmd(**kwargs: Unpack[TestPmdParamsDict]):
+            params = TestPmdParams(**kwargs)
+"""
+
+from pathlib import PurePath
+from typing import TypedDict
+
+from framework.params import Switch, YesNoSwitch
+from framework.params.testpmd import (
+    AnonMempoolAllocationMode,
+    EthPeer,
+    Event,
+    FlowGenForwardingMode,
+    HairpinMode,
+    NoisyForwardingMode,
+    Params,
+    PortNUMAConfig,
+    PortTopology,
+    RingNUMAConfig,
+    RSSSetting,
+    RXMultiQueueMode,
+    RXRingParams,
+    SimpleForwardingModes,
+    SimpleMempoolAllocationMode,
+    TxIPAddrPair,
+    TXOnlyForwardingMode,
+    TXRingParams,
+    TxUDPPortPair,
+)
+from framework.testbed_model.cpu import LogicalCoreList
+from framework.testbed_model.port import Port
+from framework.testbed_model.virtual_device import VirtualDevice
+
+
+class EalParamsDict(TypedDict, total=False):
+    """:class:`TypedDict` equivalent of :class:`~.eal.EalParams`."""
+
+    lcore_list: LogicalCoreList | None
+    memory_channels: int | None
+    prefix: str
+    no_pci: Switch
+    vdevs: list[VirtualDevice] | None
+    ports: list[Port] | None
+    other_eal_param: Params | None
+
+
+class TestPmdParamsDict(EalParamsDict, total=False):
+    """:class:`TypedDict` equivalent of :class:`~.testpmd.TestPmdParams`."""
+
+    interactive_mode: Switch
+    auto_start: Switch
+    tx_first: Switch
+    stats_period: int | None
+    display_xstats: list[str] | None
+    nb_cores: int | None
+    coremask: int | None
+    nb_ports: int | None
+    port_topology: PortTopology | None
+    portmask: int | None
+    portlist: str | None
+    numa: YesNoSwitch
+    socket_num: int | None
+    port_numa_config: list[PortNUMAConfig] | None
+    ring_numa_config: list[RingNUMAConfig] | None
+    total_num_mbufs: int | None
+    mbuf_size: list[int] | None
+    mbcache: int | None
+    max_pkt_len: int | None
+    eth_peers_configfile: PurePath | None
+    eth_peer: list[EthPeer] | None
+    tx_ip: TxIPAddrPair | None
+    tx_udp: TxUDPPortPair | None
+    enable_lro: Switch
+    max_lro_pkt_size: int | None
+    disable_crc_strip: Switch
+    enable_scatter: Switch
+    enable_hw_vlan: Switch
+    enable_hw_vlan_filter: Switch
+    enable_hw_vlan_strip: Switch
+    enable_hw_vlan_extend: Switch
+    enable_hw_qinq_strip: Switch
+    pkt_drop_enabled: Switch
+    rss: RSSSetting | None
+    forward_mode: (
+        SimpleForwardingModes
+        | FlowGenForwardingMode
+        | TXOnlyForwardingMode
+        | NoisyForwardingMode
+        | None
+    )
+    hairpin_mode: HairpinMode | None
+    hairpin_queues: int | None
+    burst: int | None
+    enable_rx_cksum: Switch
+    rx_queues: int | None
+    rx_ring: RXRingParams | None
+    no_flush_rx: Switch
+    rx_segments_offsets: list[int] | None
+    rx_segments_length: list[int] | None
+    multi_rx_mempool: Switch
+    rx_shared_queue: Switch | int
+    rx_offloads: int | None
+    rx_mq_mode: RXMultiQueueMode | None
+    tx_queues: int | None
+    tx_ring: TXRingParams | None
+    tx_offloads: int | None
+    eth_link_speed: int | None
+    disable_link_check: Switch
+    disable_device_start: Switch
+    no_lsc_interrupt: Switch
+    no_rmv_interrupt: Switch
+    bitrate_stats: int | None
+    latencystats: int | None
+    print_events: list[Event] | None
+    mask_events: list[Event] | None
+    flow_isolate_all: Switch
+    disable_flow_flush: Switch
+    hot_plug: Switch
+    vxlan_gpe_port: int | None
+    geneve_parsed_port: int | None
+    lock_all_memory: YesNoSwitch
+    mempool_allocation_mode: SimpleMempoolAllocationMode | AnonMempoolAllocationMode | None
+    record_core_cycles: Switch
+    record_burst_status: Switch
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index 92930d7fbb..5b3a7bb9ab 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -18,8 +18,11 @@
 from pathlib import PurePath
 from typing import ClassVar
 
+from typing_extensions import Unpack
+
 from framework.exception import InteractiveCommandExecutionError
 from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
+from framework.params.types import TestPmdParamsDict
 from framework.remote_session.dpdk_shell import DPDKShell
 from framework.settings import SETTINGS
 from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
@@ -76,7 +79,7 @@ def __init__(
         ascending_cores: bool = True,
         append_prefix_timestamp: bool = True,
         start_on_init: bool = True,
-        **app_params,
+        **app_params: Unpack[TestPmdParamsDict],
     ) -> None:
         """Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs."""
         super().__init__(
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 0/8] dts: add testpmd params
  2024-05-09 11:20 ` [PATCH v2 0/8] dts: add testpmd params Luca Vizzarro
                     ` (7 preceding siblings ...)
  2024-05-09 11:20   ` [PATCH v2 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
@ 2024-05-22 15:59   ` Nicholas Pratte
  8 siblings, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-05-22 15:59 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Jeremy Spewock

In addition to the pmd_scatter suite, I refactored my jumboframes
suite to use this new module for testing purposes; everything works
great, and the format looks much better too.

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>


On Thu, May 9, 2024 at 7:21 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Hello,
>
> sending in v2:
> - refactored the params module
> - strengthened typing of the params module
> - moved the params module into its own package
> - refactored EalParams and TestPmdParams and
>   moved under the params package
> - reworked interactions between nodes and shells
> - refactored imports leading to circular dependencies
>
> Best,
> Luca
>
> ---
> Depends-on: series-31896 ("dts: update mypy and clean up")
> ---
>
> Luca Vizzarro (8):
>   dts: add params manipulation module
>   dts: use Params for interactive shells
>   dts: refactor EalParams
>   dts: remove module-wide imports
>   dts: add testpmd shell params
>   dts: use testpmd params for scatter test suite
>   dts: rework interactive shells
>   dts: use Unpack for type checking and hinting
>
>  dts/framework/params/__init__.py              | 274 ++++++++
>  dts/framework/params/eal.py                   |  50 ++
>  dts/framework/params/testpmd.py               | 608 ++++++++++++++++++
>  dts/framework/params/types.py                 | 133 ++++
>  dts/framework/remote_session/__init__.py      |   5 +-
>  dts/framework/remote_session/dpdk_shell.py    | 104 +++
>  .../remote_session/interactive_shell.py       |  83 ++-
>  dts/framework/remote_session/python_shell.py  |   4 +-
>  dts/framework/remote_session/testpmd_shell.py | 102 ++-
>  dts/framework/runner.py                       |   4 +-
>  dts/framework/test_suite.py                   |   5 +-
>  dts/framework/testbed_model/__init__.py       |   7 -
>  dts/framework/testbed_model/node.py           |  36 +-
>  dts/framework/testbed_model/os_session.py     |  38 +-
>  dts/framework/testbed_model/sut_node.py       | 182 +-----
>  .../testbed_model/traffic_generator/scapy.py  |   6 +-
>  dts/tests/TestSuite_hello_world.py            |   9 +-
>  dts/tests/TestSuite_pmd_buffer_scatter.py     |  21 +-
>  dts/tests/TestSuite_smoke_tests.py            |   4 +-
>  19 files changed, 1296 insertions(+), 379 deletions(-)
>  create mode 100644 dts/framework/params/__init__.py
>  create mode 100644 dts/framework/params/eal.py
>  create mode 100644 dts/framework/params/testpmd.py
>  create mode 100644 dts/framework/params/types.py
>  create mode 100644 dts/framework/remote_session/dpdk_shell.py
>
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 1/8] dts: add params manipulation module
  2024-05-09 11:20   ` [PATCH v2 1/8] dts: add params manipulation module Luca Vizzarro
@ 2024-05-28 15:40     ` Nicholas Pratte
  2024-05-28 21:08     ` Jeremy Spewock
  2024-06-06  9:19     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-05-28 15:40 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Jeremy Spewock, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Thu, May 9, 2024 at 7:21 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> This commit introduces a new "params" module, which adds a new way
> to manage command line parameters. The provided Params dataclass
> is able to read the fields of its child class and produce a string
> representation to supply to the command line. Any data structure
> that is intended to represent command line parameters can inherit it.
>
> The main purpose is to make it easier to represent data structures that
> map to parameters. Aiding quicker development, while minimising code
> bloat.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>  dts/framework/params/__init__.py | 274 +++++++++++++++++++++++++++++++
>  1 file changed, 274 insertions(+)
>  create mode 100644 dts/framework/params/__init__.py
>
> diff --git a/dts/framework/params/__init__.py b/dts/framework/params/__init__.py
> new file mode 100644
> index 0000000000..aa27e34357
> --- /dev/null
> +++ b/dts/framework/params/__init__.py
> @@ -0,0 +1,274 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2024 Arm Limited
> +
> +"""Parameter manipulation module.
> +
> +This module provides :class:`Params` which can be used to model any data structure
> +that is meant to represent any command parameters.
> +"""
> +
> +from dataclasses import dataclass, fields
> +from enum import Flag
> +from typing import Any, Callable, Iterable, Literal, Reversible, TypedDict, cast
> +
> +from typing_extensions import Self
> +
> +#: Type for a function taking one argument.
> +FnPtr = Callable[[Any], Any]
> +#: Type for a switch parameter.
> +Switch = Literal[True, None]
> +#: Type for a yes/no switch parameter.
> +YesNoSwitch = Literal[True, False, None]
> +
> +
> +def _reduce_functions(funcs: Reversible[FnPtr]) -> FnPtr:
> +    """Reduces an iterable of :attr:`FnPtr` from end to start to a composite function.
> +
> +    If the iterable is empty, the created function just returns its fed value back.
> +    """
> +
> +    def composite_function(value: Any):
> +        for fn in reversed(funcs):
> +            value = fn(value)
> +        return value
> +
> +    return composite_function
> +
> +
> +def convert_str(*funcs: FnPtr):
> +    """Decorator that makes the ``__str__`` method a composite function created from its arguments.
> +
> +    The :attr:`FnPtr`s fed to the decorator are executed from right to left
> +    in the arguments list order.
> +
> +    Example:
> +    .. code:: python
> +
> +        @convert_str(hex_from_flag_value)
> +        class BitMask(enum.Flag):
> +            A = auto()
> +            B = auto()
> +
> +    will allow ``BitMask`` to render as a hexadecimal value.
> +    """
> +
> +    def _class_decorator(original_class):
> +        original_class.__str__ = _reduce_functions(funcs)
> +        return original_class
> +
> +    return _class_decorator
> +
> +
> +def comma_separated(values: Iterable[Any]) -> str:
> +    """Converts an iterable in a comma-separated string."""
> +    return ",".join([str(value).strip() for value in values if value is not None])
> +
> +
> +def bracketed(value: str) -> str:
> +    """Adds round brackets to the input."""
> +    return f"({value})"
> +
> +
> +def str_from_flag_value(flag: Flag) -> str:
> +    """Returns the value from a :class:`enum.Flag` as a string."""
> +    return str(flag.value)
> +
> +
> +def hex_from_flag_value(flag: Flag) -> str:
> +    """Returns the value from a :class:`enum.Flag` converted to hexadecimal."""
> +    return hex(flag.value)
> +
> +
> +class ParamsModifier(TypedDict, total=False):
> +    """Params modifiers dict compatible with the :func:`dataclasses.field` metadata parameter."""
> +
> +    #:
> +    Params_value_only: bool
> +    #:
> +    Params_short: str
> +    #:
> +    Params_long: str
> +    #:
> +    Params_multiple: bool
> +    #:
> +    Params_convert_value: Reversible[FnPtr]
> +
> +
> +@dataclass
> +class Params:
> +    """Dataclass that renders its fields into command line arguments.
> +
> +    The parameter name is taken from the field name by default. The following:
> +
> +    .. code:: python
> +
> +        name: str | None = "value"
> +
> +    is rendered as ``--name=value``.
> +    Through :func:`dataclasses.field` the resulting parameter can be manipulated by applying
> +    this class' metadata modifier functions.
> +
> +    To use fields as switches, set the value to ``True`` to render them. If you
> +    use a yes/no switch you can also set ``False`` which would render a switch
> +    prefixed with ``--no-``. Examples:
> +
> +    .. code:: python
> +
> +        interactive: Switch = True  # renders --interactive
> +        numa: YesNoSwitch   = False # renders --no-numa
> +
> +    Setting ``None`` will prevent it from being rendered. The :attr:`~Switch` type alias is provided
> +    for regular switches, whereas :attr:`~YesNoSwitch` is offered for yes/no ones.
> +
> +    An instance of a dataclass inheriting ``Params`` can also be assigned to an attribute,
> +    this helps with grouping parameters together.
> +    The attribute holding the dataclass will be ignored and the latter will just be rendered as
> +    expected.
> +    """
> +
> +    _suffix = ""
> +    """Holder of the plain text value of Params when called directly. A suffix for child classes."""
> +
> +    """========= BEGIN FIELD METADATA MODIFIER FUNCTIONS ========"""
> +
> +    @staticmethod
> +    def value_only() -> ParamsModifier:
> +        """Injects the value of the attribute as-is without flag.
> +
> +        Metadata modifier for :func:`dataclasses.field`.
> +        """
> +        return ParamsModifier(Params_value_only=True)
> +
> +    @staticmethod
> +    def short(name: str) -> ParamsModifier:
> +        """Overrides any parameter name with the given short option.
> +
> +        Metadata modifier for :func:`dataclasses.field`.
> +
> +        Example:
> +        .. code:: python
> +
> +            logical_cores: str | None = field(default="1-4", metadata=Params.short("l"))
> +
> +        will render as ``-l=1-4`` instead of ``--logical-cores=1-4``.
> +        """
> +        return ParamsModifier(Params_short=name)
> +
> +    @staticmethod
> +    def long(name: str) -> ParamsModifier:
> +        """Overrides the inferred parameter name to the specified one.
> +
> +        Metadata modifier for :func:`dataclasses.field`.
> +
> +        Example:
> +        .. code:: python
> +
> +            x_name: str | None = field(default="y", metadata=Params.long("x"))
> +
> +        will render as ``--x=y``, but the field is accessed and modified through ``x_name``.
> +        """
> +        return ParamsModifier(Params_long=name)
> +
> +    @staticmethod
> +    def multiple() -> ParamsModifier:
> +        """Specifies that this parameter is set multiple times. Must be a list.
> +
> +        Metadata modifier for :func:`dataclasses.field`.
> +
> +        Example:
> +        .. code:: python
> +
> +            ports: list[int] | None = field(
> +                default_factory=lambda: [0, 1, 2],
> +                metadata=Params.multiple() | Params.long("port")
> +            )
> +
> +        will render as ``--port=0 --port=1 --port=2``. Note that modifiers can be chained like
> +        in this example.
> +        """
> +        return ParamsModifier(Params_multiple=True)
> +
> +    @classmethod
> +    def convert_value(cls, *funcs: FnPtr) -> ParamsModifier:
> +        """Takes in a variable number of functions to convert the value text representation.
> +
> +        Metadata modifier for :func:`dataclasses.field`.
> +
> +        The ``metadata`` keyword argument can be used to chain metadata modifiers together.
> +
> +        Functions can be chained together, executed from right to left in the arguments list order.
> +
> +        Example:
> +        .. code:: python
> +
> +            hex_bitmask: int | None = field(
> +                default=0b1101,
> +                metadata=Params.convert_value(hex) | Params.long("mask")
> +            )
> +
> +        will render as ``--mask=0xd``.
> +        """
> +        return ParamsModifier(Params_convert_value=funcs)
> +
> +    """========= END FIELD METADATA MODIFIER FUNCTIONS ========"""
> +
> +    def append_str(self, text: str) -> None:
> +        """Appends a string at the end of the string representation."""
> +        self._suffix += text
> +
> +    def __iadd__(self, text: str) -> Self:
> +        """Appends a string at the end of the string representation."""
> +        self.append_str(text)
> +        return self
> +
> +    @classmethod
> +    def from_str(cls, text: str) -> Self:
> +        """Creates a plain Params object from a string."""
> +        obj = cls()
> +        obj.append_str(text)
> +        return obj
> +
> +    @staticmethod
> +    def _make_switch(
> +        name: str, is_short: bool = False, is_no: bool = False, value: str | None = None
> +    ) -> str:
> +        prefix = f"{'-' if is_short else '--'}{'no-' if is_no else ''}"
> +        name = name.replace("_", "-")
> +        value = f"{' ' if is_short else '='}{value}" if value else ""
> +        return f"{prefix}{name}{value}"
> +
> +    def __str__(self) -> str:
> +        """Returns a string of command-line-ready arguments from the class fields."""
> +        arguments: list[str] = []
> +
> +        for field in fields(self):
> +            value = getattr(self, field.name)
> +            modifiers = cast(ParamsModifier, field.metadata)
> +
> +            if value is None:
> +                continue
> +
> +            value_only = modifiers.get("Params_value_only", False)
> +            if isinstance(value, Params) or value_only:
> +                arguments.append(str(value))
> +                continue
> +
> +            # take the short modifier, or the long modifier, or infer from field name
> +            switch_name = modifiers.get("Params_short", modifiers.get("Params_long", field.name))
> +            is_short = "Params_short" in modifiers
> +
> +            if isinstance(value, bool):
> +                arguments.append(self._make_switch(switch_name, is_short, is_no=(not value)))
> +                continue
> +
> +            convert = _reduce_functions(modifiers.get("Params_convert_value", []))
> +            multiple = modifiers.get("Params_multiple", False)
> +
> +            values = value if multiple else [value]
> +            for value in values:
> +                arguments.append(self._make_switch(switch_name, is_short, value=convert(value)))
> +
> +        if self._suffix:
> +            arguments.append(self._suffix)
> +
> +        return " ".join(arguments)
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH 2/6] dts: use Params for interactive shells
  2024-03-26 19:04 ` [PATCH 2/6] dts: use Params for interactive shells Luca Vizzarro
  2024-03-28 16:48   ` Jeremy Spewock
@ 2024-05-28 15:43   ` Nicholas Pratte
  1 sibling, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-05-28 15:43 UTC (permalink / raw)
  To: Luca Vizzarro
  Cc: dev, Juraj Linkeš, Jack Bond-Preston, Honnappa Nagarahalli

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Tue, Mar 26, 2024 at 3:04 PM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Make it so that interactive shells accept an implementation of `Params`
> for app arguments. Convert EalParameters to use `Params` instead.
>
> String command line parameters can still be supplied by using the
> `StrParams` implementation.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Jack Bond-Preston <jack.bond-preston@arm.com>
> Reviewed-by: Honnappa Nagarahalli <honnappa.nagarahalli@arm.com>
> ---
>  .../remote_session/interactive_shell.py       |   8 +-
>  dts/framework/remote_session/testpmd_shell.py |  12 +-
>  dts/framework/testbed_model/__init__.py       |   2 +-
>  dts/framework/testbed_model/node.py           |   4 +-
>  dts/framework/testbed_model/os_session.py     |   4 +-
>  dts/framework/testbed_model/sut_node.py       | 106 ++++++++----------
>  dts/tests/TestSuite_pmd_buffer_scatter.py     |   3 +-
>  7 files changed, 73 insertions(+), 66 deletions(-)
>
> diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
> index 5cfe202e15..a2c7b30d9f 100644
> --- a/dts/framework/remote_session/interactive_shell.py
> +++ b/dts/framework/remote_session/interactive_shell.py
> @@ -1,5 +1,6 @@
>  # SPDX-License-Identifier: BSD-3-Clause
>  # Copyright(c) 2023 University of New Hampshire
> +# Copyright(c) 2024 Arm Limited
>
>  """Common functionality for interactive shell handling.
>
> @@ -21,6 +22,7 @@
>  from paramiko import Channel, SSHClient, channel  # type: ignore[import]
>
>  from framework.logger import DTSLogger
> +from framework.params import Params
>  from framework.settings import SETTINGS
>
>
> @@ -40,7 +42,7 @@ class InteractiveShell(ABC):
>      _ssh_channel: Channel
>      _logger: DTSLogger
>      _timeout: float
> -    _app_args: str
> +    _app_args: Params | None
>
>      #: Prompt to expect at the end of output when sending a command.
>      #: This is often overridden by subclasses.
> @@ -63,7 +65,7 @@ def __init__(
>          interactive_session: SSHClient,
>          logger: DTSLogger,
>          get_privileged_command: Callable[[str], str] | None,
> -        app_args: str = "",
> +        app_args: Params | None = None,
>          timeout: float = SETTINGS.timeout,
>      ) -> None:
>          """Create an SSH channel during initialization.
> @@ -100,7 +102,7 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
>              get_privileged_command: A function (but could be any callable) that produces
>                  the version of the command with elevated privileges.
>          """
> -        start_command = f"{self.path} {self._app_args}"
> +        start_command = f"{self.path} {self._app_args or ''}"
>          if get_privileged_command is not None:
>              start_command = get_privileged_command(start_command)
>          self.send_command(start_command)
> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
> index cb2ab6bd00..db3abb7600 100644
> --- a/dts/framework/remote_session/testpmd_shell.py
> +++ b/dts/framework/remote_session/testpmd_shell.py
> @@ -21,6 +21,7 @@
>  from typing import Callable, ClassVar
>
>  from framework.exception import InteractiveCommandExecutionError
> +from framework.params import StrParams
>  from framework.settings import SETTINGS
>  from framework.utils import StrEnum
>
> @@ -118,8 +119,15 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
>          Also find the number of pci addresses which were allowed on the command line when the app
>          was started.
>          """
> -        self._app_args += " -i --mask-event intr_lsc"
> -        self.number_of_ports = self._app_args.count("-a ")
> +        from framework.testbed_model.sut_node import EalParameters
> +
> +        assert isinstance(self._app_args, EalParameters)
> +
> +        if isinstance(self._app_args.app_params, StrParams):
> +            self._app_args.app_params.value += " -i --mask-event intr_lsc"
> +
> +        self.number_of_ports = len(self._app_args.ports) if self._app_args.ports is not None else 0
> +
>          super()._start_application(get_privileged_command)
>
>      def start(self, verify: bool = True) -> None:
> diff --git a/dts/framework/testbed_model/__init__.py b/dts/framework/testbed_model/__init__.py
> index 6086512ca2..ef9520df4c 100644
> --- a/dts/framework/testbed_model/__init__.py
> +++ b/dts/framework/testbed_model/__init__.py
> @@ -23,6 +23,6 @@
>  from .cpu import LogicalCoreCount, LogicalCoreCountFilter, LogicalCoreList
>  from .node import Node
>  from .port import Port, PortLink
> -from .sut_node import SutNode
> +from .sut_node import SutNode, EalParameters
>  from .tg_node import TGNode
>  from .virtual_device import VirtualDevice
> diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
> index 74061f6262..ec9512d618 100644
> --- a/dts/framework/testbed_model/node.py
> +++ b/dts/framework/testbed_model/node.py
> @@ -2,6 +2,7 @@
>  # Copyright(c) 2010-2014 Intel Corporation
>  # Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
>  # Copyright(c) 2022-2023 University of New Hampshire
> +# Copyright(c) 2024 Arm Limited
>
>  """Common functionality for node management.
>
> @@ -24,6 +25,7 @@
>  )
>  from framework.exception import ConfigurationError
>  from framework.logger import DTSLogger, get_dts_logger
> +from framework.params import Params
>  from framework.settings import SETTINGS
>
>  from .cpu import (
> @@ -199,7 +201,7 @@ def create_interactive_shell(
>          shell_cls: Type[InteractiveShellType],
>          timeout: float = SETTINGS.timeout,
>          privileged: bool = False,
> -        app_args: str = "",
> +        app_args: Params | None = None,
>      ) -> InteractiveShellType:
>          """Factory for interactive session handlers.
>
> diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
> index d5bf7e0401..7234c975c8 100644
> --- a/dts/framework/testbed_model/os_session.py
> +++ b/dts/framework/testbed_model/os_session.py
> @@ -1,6 +1,7 @@
>  # SPDX-License-Identifier: BSD-3-Clause
>  # Copyright(c) 2023 PANTHEON.tech s.r.o.
>  # Copyright(c) 2023 University of New Hampshire
> +# Copyright(c) 2024 Arm Limited
>
>  """OS-aware remote session.
>
> @@ -29,6 +30,7 @@
>
>  from framework.config import Architecture, NodeConfiguration, NodeInfo
>  from framework.logger import DTSLogger
> +from framework.params import Params
>  from framework.remote_session import (
>      CommandResult,
>      InteractiveRemoteSession,
> @@ -134,7 +136,7 @@ def create_interactive_shell(
>          shell_cls: Type[InteractiveShellType],
>          timeout: float,
>          privileged: bool,
> -        app_args: str,
> +        app_args: Params | None,
>      ) -> InteractiveShellType:
>          """Factory for interactive session handlers.
>
> diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
> index 97aa26d419..3f8c3807b3 100644
> --- a/dts/framework/testbed_model/sut_node.py
> +++ b/dts/framework/testbed_model/sut_node.py
> @@ -2,6 +2,7 @@
>  # Copyright(c) 2010-2014 Intel Corporation
>  # Copyright(c) 2023 PANTHEON.tech s.r.o.
>  # Copyright(c) 2023 University of New Hampshire
> +# Copyright(c) 2024 Arm Limited
>
>  """System under test (DPDK + hardware) node.
>
> @@ -11,6 +12,7 @@
>  """
>
>
> +from dataclasses import dataclass, field
>  import os
>  import tarfile
>  import time
> @@ -23,6 +25,8 @@
>      NodeInfo,
>      SutNodeConfiguration,
>  )
> +from framework import params
> +from framework.params import Params, StrParams
>  from framework.remote_session import CommandResult
>  from framework.settings import SETTINGS
>  from framework.utils import MesonArgs
> @@ -34,62 +38,51 @@
>  from .virtual_device import VirtualDevice
>
>
> -class EalParameters(object):
> +def _port_to_pci(port: Port) -> str:
> +    return port.pci
> +
> +
> +@dataclass(kw_only=True)
> +class EalParameters(Params):
>      """The environment abstraction layer parameters.
>
>      The string representation can be created by converting the instance to a string.
>      """
>
> -    def __init__(
> -        self,
> -        lcore_list: LogicalCoreList,
> -        memory_channels: int,
> -        prefix: str,
> -        no_pci: bool,
> -        vdevs: list[VirtualDevice],
> -        ports: list[Port],
> -        other_eal_param: str,
> -    ):
> -        """Initialize the parameters according to inputs.
> -
> -        Process the parameters into the format used on the command line.
> +    lcore_list: LogicalCoreList = field(metadata=params.short("l"))
> +    """The list of logical cores to use."""
>
> -        Args:
> -            lcore_list: The list of logical cores to use.
> -            memory_channels: The number of memory channels to use.
> -            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
> -            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
> -            vdevs: Virtual devices, e.g.::
> +    memory_channels: int = field(metadata=params.short("n"))
> +    """The number of memory channels to use."""
>
> -                vdevs=[
> -                    VirtualDevice('net_ring0'),
> -                    VirtualDevice('net_ring1')
> -                ]
> -            ports: The list of ports to allow.
> -            other_eal_param: user defined DPDK EAL parameters, e.g.:
> -                ``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._ports = " ".join(f"-a {port.pci}" for port in ports)
> -        self._other_eal_param = other_eal_param
> -
> -    def __str__(self) -> str:
> -        """Create the EAL string."""
> -        return (
> -            f"{self._lcore_list} "
> -            f"{self._memory_channels} "
> -            f"{self._prefix} "
> -            f"{self._no_pci} "
> -            f"{self._vdevs} "
> -            f"{self._ports} "
> -            f"{self._other_eal_param}"
> -        )
> +    prefix: str = field(metadata=params.long("file-prefix"))
> +    """Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``."""
> +
> +    no_pci: params.Option
> +    """Switch to disable PCI bus e.g.: ``no_pci=True``."""
> +
> +    vdevs: list[VirtualDevice] | None = field(
> +        default=None, metadata=params.multiple(params.long("vdev"))
> +    )
> +    """Virtual devices, e.g.::
> +
> +        vdevs=[
> +            VirtualDevice("net_ring0"),
> +            VirtualDevice("net_ring1")
> +        ]
> +    """
> +
> +    ports: list[Port] | None = field(
> +        default=None,
> +        metadata=params.field_mixins(_port_to_pci, metadata=params.multiple(params.short("a"))),
> +    )
> +    """The list of ports to allow."""
> +
> +    other_eal_param: StrParams | None = None
> +    """Any other EAL parameter(s)."""
> +
> +    app_params: Params | None = field(default=None, metadata=params.options_end())
> +    """Parameters to pass to the underlying DPDK app."""
>
>
>  class SutNode(Node):
> @@ -350,7 +343,7 @@ def create_eal_parameters(
>          ascending_cores: bool = True,
>          prefix: str = "dpdk",
>          append_prefix_timestamp: bool = True,
> -        no_pci: bool = False,
> +        no_pci: params.Option = None,
>          vdevs: list[VirtualDevice] | None = None,
>          ports: list[Port] | None = None,
>          other_eal_param: str = "",
> @@ -393,9 +386,6 @@ def create_eal_parameters(
>          if prefix:
>              self._dpdk_prefix_list.append(prefix)
>
> -        if vdevs is None:
> -            vdevs = []
> -
>          if ports is None:
>              ports = self.ports
>
> @@ -406,7 +396,7 @@ def create_eal_parameters(
>              no_pci=no_pci,
>              vdevs=vdevs,
>              ports=ports,
> -            other_eal_param=other_eal_param,
> +            other_eal_param=StrParams(other_eal_param),
>          )
>
>      def run_dpdk_app(
> @@ -442,7 +432,7 @@ def create_interactive_shell(
>          shell_cls: Type[InteractiveShellType],
>          timeout: float = SETTINGS.timeout,
>          privileged: bool = False,
> -        app_parameters: str = "",
> +        app_parameters: Params | None = None,
>          eal_parameters: EalParameters | None = None,
>      ) -> InteractiveShellType:
>          """Extend the factory for interactive session handlers.
> @@ -459,6 +449,7 @@ def create_interactive_shell(
>                  reading from the buffer and don't receive any data within the timeout
>                  it will throw an error.
>              privileged: Whether to run the shell with administrative privileges.
> +            app_args: The arguments to be passed to the application.
>              eal_parameters: List of EAL parameters to use to launch the app. If this
>                  isn't provided or an empty string is passed, it will default to calling
>                  :meth:`create_eal_parameters`.
> @@ -470,9 +461,10 @@ def create_interactive_shell(
>          """
>          # We need to append the build directory and add EAL parameters for DPDK apps
>          if shell_cls.dpdk_app:
> -            if not eal_parameters:
> +            if eal_parameters is None:
>                  eal_parameters = self.create_eal_parameters()
> -            app_parameters = f"{eal_parameters} -- {app_parameters}"
> +            eal_parameters.app_params = app_parameters
> +            app_parameters = eal_parameters
>
>              shell_cls.path = self.main_session.join_remote_path(
>                  self.remote_dpdk_build_dir, shell_cls.path
> diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
> index 3701c47408..4cdbdc4272 100644
> --- a/dts/tests/TestSuite_pmd_buffer_scatter.py
> +++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
> @@ -22,6 +22,7 @@
>  from scapy.packet import Raw  # type: ignore[import]
>  from scapy.utils import hexstr  # type: ignore[import]
>
> +from framework.params import StrParams
>  from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
>  from framework.test_suite import TestSuite
>
> @@ -103,7 +104,7 @@ def pmd_scatter(self, mbsize: int) -> None:
>          """
>          testpmd = self.sut_node.create_interactive_shell(
>              TestPmdShell,
> -            app_parameters=(
> +            app_parameters=StrParams(
>                  "--mbcache=200 "
>                  f"--mbuf-size={mbsize} "
>                  "--max-pkt-len=9000 "
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 3/8] dts: refactor EalParams
  2024-05-09 11:20   ` [PATCH v2 3/8] dts: refactor EalParams Luca Vizzarro
@ 2024-05-28 15:44     ` Nicholas Pratte
  2024-05-28 21:05     ` Jeremy Spewock
  2024-06-06 13:17     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-05-28 15:44 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Jeremy Spewock, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Thu, May 9, 2024 at 7:21 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Move EalParams to its own module to avoid circular dependencies.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>  dts/framework/params/eal.py                   | 50 +++++++++++++++++++
>  dts/framework/remote_session/testpmd_shell.py |  2 +-
>  dts/framework/testbed_model/sut_node.py       | 42 +---------------
>  3 files changed, 53 insertions(+), 41 deletions(-)
>  create mode 100644 dts/framework/params/eal.py
>
> diff --git a/dts/framework/params/eal.py b/dts/framework/params/eal.py
> new file mode 100644
> index 0000000000..bbdbc8f334
> --- /dev/null
> +++ b/dts/framework/params/eal.py
> @@ -0,0 +1,50 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2024 Arm Limited
> +
> +"""Module representing the DPDK EAL-related parameters."""
> +
> +from dataclasses import dataclass, field
> +from typing import Literal
> +
> +from framework.params import Params, Switch
> +from framework.testbed_model.cpu import LogicalCoreList
> +from framework.testbed_model.port import Port
> +from framework.testbed_model.virtual_device import VirtualDevice
> +
> +
> +def _port_to_pci(port: Port) -> str:
> +    return port.pci
> +
> +
> +@dataclass(kw_only=True)
> +class EalParams(Params):
> +    """The environment abstraction layer parameters.
> +
> +    Attributes:
> +        lcore_list: The list of logical cores to use.
> +        memory_channels: The number of memory channels to use.
> +        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
> +        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
> +        vdevs: Virtual devices, e.g.::
> +            vdevs=[
> +                VirtualDevice('net_ring0'),
> +                VirtualDevice('net_ring1')
> +            ]
> +        ports: The list of ports to allow.
> +        other_eal_param: user defined DPDK EAL parameters, e.g.:
> +                ``other_eal_param='--single-file-segments'``
> +    """
> +
> +    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
> +    memory_channels: int = field(metadata=Params.short("n"))
> +    prefix: str = field(metadata=Params.long("file-prefix"))
> +    no_pci: Switch = None
> +    vdevs: list[VirtualDevice] | None = field(
> +        default=None, metadata=Params.multiple() | Params.long("vdev")
> +    )
> +    ports: list[Port] | None = field(
> +        default=None,
> +        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
> +    )
> +    other_eal_param: Params | None = None
> +    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
> index 7eced27096..841d456a2f 100644
> --- a/dts/framework/remote_session/testpmd_shell.py
> +++ b/dts/framework/remote_session/testpmd_shell.py
> @@ -21,8 +21,8 @@
>  from typing import Callable, ClassVar
>
>  from framework.exception import InteractiveCommandExecutionError
> +from framework.params.eal import EalParams
>  from framework.settings import SETTINGS
> -from framework.testbed_model.sut_node import EalParams
>  from framework.utils import StrEnum
>
>  from .interactive_shell import InteractiveShell
> diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
> index c886590979..e1163106a3 100644
> --- a/dts/framework/testbed_model/sut_node.py
> +++ b/dts/framework/testbed_model/sut_node.py
> @@ -15,9 +15,8 @@
>  import os
>  import tarfile
>  import time
> -from dataclasses import dataclass, field
>  from pathlib import PurePath
> -from typing import Literal, Type
> +from typing import Type
>
>  from framework.config import (
>      BuildTargetConfiguration,
> @@ -26,6 +25,7 @@
>      SutNodeConfiguration,
>  )
>  from framework.params import Params, Switch
> +from framework.params.eal import EalParams
>  from framework.remote_session import CommandResult
>  from framework.settings import SETTINGS
>  from framework.utils import MesonArgs
> @@ -37,44 +37,6 @@
>  from .virtual_device import VirtualDevice
>
>
> -def _port_to_pci(port: Port) -> str:
> -    return port.pci
> -
> -
> -@dataclass(kw_only=True)
> -class EalParams(Params):
> -    """The environment abstraction layer parameters.
> -
> -    Attributes:
> -        lcore_list: The list of logical cores to use.
> -        memory_channels: The number of memory channels to use.
> -        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
> -        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
> -        vdevs: Virtual devices, e.g.::
> -            vdevs=[
> -                VirtualDevice('net_ring0'),
> -                VirtualDevice('net_ring1')
> -            ]
> -        ports: The list of ports to allow.
> -        other_eal_param: user defined DPDK EAL parameters, e.g.:
> -                ``other_eal_param='--single-file-segments'``
> -    """
> -
> -    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
> -    memory_channels: int = field(metadata=Params.short("n"))
> -    prefix: str = field(metadata=Params.long("file-prefix"))
> -    no_pci: Switch
> -    vdevs: list[VirtualDevice] | None = field(
> -        default=None, metadata=Params.multiple() | Params.long("vdev")
> -    )
> -    ports: list[Port] | None = field(
> -        default=None,
> -        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
> -    )
> -    other_eal_param: Params | None = None
> -    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
> -
> -
>  class SutNode(Node):
>      """The system under test node.
>
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 4/8] dts: remove module-wide imports
  2024-05-09 11:20   ` [PATCH v2 4/8] dts: remove module-wide imports Luca Vizzarro
@ 2024-05-28 15:45     ` Nicholas Pratte
  2024-05-28 21:08     ` Jeremy Spewock
  2024-06-06 13:21     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-05-28 15:45 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Jeremy Spewock, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Thu, May 9, 2024 at 7:21 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Remove the imports in the testbed_model and remote_session modules init
> file, to avoid the initialisation of unneeded modules, thus removing or
> limiting the risk of circular dependencies.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>  dts/framework/remote_session/__init__.py               | 5 +----
>  dts/framework/runner.py                                | 4 +++-
>  dts/framework/test_suite.py                            | 5 ++++-
>  dts/framework/testbed_model/__init__.py                | 7 -------
>  dts/framework/testbed_model/os_session.py              | 4 ++--
>  dts/framework/testbed_model/sut_node.py                | 2 +-
>  dts/framework/testbed_model/traffic_generator/scapy.py | 2 +-
>  dts/tests/TestSuite_hello_world.py                     | 2 +-
>  dts/tests/TestSuite_smoke_tests.py                     | 2 +-
>  9 files changed, 14 insertions(+), 19 deletions(-)
>
> diff --git a/dts/framework/remote_session/__init__.py b/dts/framework/remote_session/__init__.py
> index 1910c81c3c..29000a4642 100644
> --- a/dts/framework/remote_session/__init__.py
> +++ b/dts/framework/remote_session/__init__.py
> @@ -18,11 +18,8 @@
>  from framework.logger import DTSLogger
>
>  from .interactive_remote_session import InteractiveRemoteSession
> -from .interactive_shell import InteractiveShell
> -from .python_shell import PythonShell
> -from .remote_session import CommandResult, RemoteSession
> +from .remote_session import RemoteSession
>  from .ssh_session import SSHSession
> -from .testpmd_shell import TestPmdShell
>
>
>  def create_remote_session(
> diff --git a/dts/framework/runner.py b/dts/framework/runner.py
> index d74f1871db..e6c23af7c7 100644
> --- a/dts/framework/runner.py
> +++ b/dts/framework/runner.py
> @@ -26,6 +26,9 @@
>  from types import FunctionType
>  from typing import Iterable, Sequence
>
> +from framework.testbed_model.sut_node import SutNode
> +from framework.testbed_model.tg_node import TGNode
> +
>  from .config import (
>      BuildTargetConfiguration,
>      Configuration,
> @@ -51,7 +54,6 @@
>      TestSuiteWithCases,
>  )
>  from .test_suite import TestSuite
> -from .testbed_model import SutNode, TGNode
>
>
>  class DTSRunner:
> diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py
> index 8768f756a6..9d3debb00f 100644
> --- a/dts/framework/test_suite.py
> +++ b/dts/framework/test_suite.py
> @@ -20,9 +20,12 @@
>  from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
>  from scapy.packet import Packet, Padding  # type: ignore[import-untyped]
>
> +from framework.testbed_model.port import Port, PortLink
> +from framework.testbed_model.sut_node import SutNode
> +from framework.testbed_model.tg_node import TGNode
> +
>  from .exception import TestCaseVerifyError
>  from .logger import DTSLogger, get_dts_logger
> -from .testbed_model import Port, PortLink, SutNode, TGNode
>  from .testbed_model.traffic_generator import PacketFilteringConfig
>  from .utils import get_packet_summaries
>
> diff --git a/dts/framework/testbed_model/__init__.py b/dts/framework/testbed_model/__init__.py
> index 6086512ca2..4f8a58c039 100644
> --- a/dts/framework/testbed_model/__init__.py
> +++ b/dts/framework/testbed_model/__init__.py
> @@ -19,10 +19,3 @@
>  """
>
>  # pylama:ignore=W0611
> -
> -from .cpu import LogicalCoreCount, LogicalCoreCountFilter, LogicalCoreList
> -from .node import Node
> -from .port import Port, PortLink
> -from .sut_node import SutNode
> -from .tg_node import TGNode
> -from .virtual_device import VirtualDevice
> diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
> index 1a77aee532..e5f5fcbe0e 100644
> --- a/dts/framework/testbed_model/os_session.py
> +++ b/dts/framework/testbed_model/os_session.py
> @@ -32,13 +32,13 @@
>  from framework.logger import DTSLogger
>  from framework.params import Params
>  from framework.remote_session import (
> -    CommandResult,
>      InteractiveRemoteSession,
> -    InteractiveShell,
>      RemoteSession,
>      create_interactive_session,
>      create_remote_session,
>  )
> +from framework.remote_session.interactive_shell import InteractiveShell
> +from framework.remote_session.remote_session import CommandResult
>  from framework.settings import SETTINGS
>  from framework.utils import MesonArgs
>
> diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
> index e1163106a3..83ad06ae2d 100644
> --- a/dts/framework/testbed_model/sut_node.py
> +++ b/dts/framework/testbed_model/sut_node.py
> @@ -26,7 +26,7 @@
>  )
>  from framework.params import Params, Switch
>  from framework.params.eal import EalParams
> -from framework.remote_session import CommandResult
> +from framework.remote_session.remote_session import CommandResult
>  from framework.settings import SETTINGS
>  from framework.utils import MesonArgs
>
> diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py
> index ed5467d825..7bc1c2cc08 100644
> --- a/dts/framework/testbed_model/traffic_generator/scapy.py
> +++ b/dts/framework/testbed_model/traffic_generator/scapy.py
> @@ -25,7 +25,7 @@
>  from scapy.packet import Packet  # type: ignore[import-untyped]
>
>  from framework.config import OS, ScapyTrafficGeneratorConfig
> -from framework.remote_session import PythonShell
> +from framework.remote_session.python_shell import PythonShell
>  from framework.settings import SETTINGS
>  from framework.testbed_model.node import Node
>  from framework.testbed_model.port import Port
> diff --git a/dts/tests/TestSuite_hello_world.py b/dts/tests/TestSuite_hello_world.py
> index fd7ff1534d..0d6995f260 100644
> --- a/dts/tests/TestSuite_hello_world.py
> +++ b/dts/tests/TestSuite_hello_world.py
> @@ -8,7 +8,7 @@
>  """
>
>  from framework.test_suite import TestSuite
> -from framework.testbed_model import (
> +from framework.testbed_model.cpu import (
>      LogicalCoreCount,
>      LogicalCoreCountFilter,
>      LogicalCoreList,
> diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py
> index a553e89662..ca678f662d 100644
> --- a/dts/tests/TestSuite_smoke_tests.py
> +++ b/dts/tests/TestSuite_smoke_tests.py
> @@ -15,7 +15,7 @@
>  import re
>
>  from framework.config import PortConfig
> -from framework.remote_session import TestPmdShell
> +from framework.remote_session.testpmd_shell import TestPmdShell
>  from framework.settings import SETTINGS
>  from framework.test_suite import TestSuite
>  from framework.utils import REGEX_FOR_PCI_ADDRESS
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 6/8] dts: use testpmd params for scatter test suite
  2024-05-09 11:20   ` [PATCH v2 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
@ 2024-05-28 15:49     ` Nicholas Pratte
  2024-05-28 21:06       ` Jeremy Spewock
  0 siblings, 1 reply; 159+ messages in thread
From: Nicholas Pratte @ 2024-05-28 15:49 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Jeremy Spewock, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Thu, May 9, 2024 at 7:21 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Update the buffer scatter test suite to use TestPmdParameters
> instead of the StrParams implementation.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>  dts/tests/TestSuite_pmd_buffer_scatter.py | 18 +++++++++---------
>  1 file changed, 9 insertions(+), 9 deletions(-)
>
> diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
> index 578b5a4318..6d206c1a40 100644
> --- a/dts/tests/TestSuite_pmd_buffer_scatter.py
> +++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
> @@ -16,14 +16,14 @@
>  """
>
>  import struct
> +from dataclasses import asdict
>
>  from scapy.layers.inet import IP  # type: ignore[import-untyped]
>  from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
>  from scapy.packet import Raw  # type: ignore[import-untyped]
>  from scapy.utils import hexstr  # type: ignore[import-untyped]
>
> -from framework.params import Params
> -from framework.params.testpmd import SimpleForwardingModes
> +from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
>  from framework.remote_session.testpmd_shell import TestPmdShell
>  from framework.test_suite import TestSuite
>
> @@ -105,16 +105,16 @@ def pmd_scatter(self, mbsize: int) -> None:
>          """
>          testpmd = self.sut_node.create_interactive_shell(
>              TestPmdShell,
> -            app_params=Params.from_str(
> -                "--mbcache=200 "
> -                f"--mbuf-size={mbsize} "
> -                "--max-pkt-len=9000 "
> -                "--port-topology=paired "
> -                "--tx-offloads=0x00008000"
> +            app_params=TestPmdParams(
> +                forward_mode=SimpleForwardingModes.mac,
> +                mbcache=200,
> +                mbuf_size=[mbsize],
> +                max_pkt_len=9000,
> +                tx_offloads=0x00008000,
> +                **asdict(self.sut_node.create_eal_parameters()),
>              ),
>              privileged=True,
>          )
> -        testpmd.set_forward_mode(SimpleForwardingModes.mac)
>          testpmd.start()
>
>          for offset in [-1, 0, 1, 4, 5]:
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 7/8] dts: rework interactive shells
  2024-05-09 11:20   ` [PATCH v2 7/8] dts: rework interactive shells Luca Vizzarro
@ 2024-05-28 15:50     ` Nicholas Pratte
  2024-05-28 21:07     ` Jeremy Spewock
  1 sibling, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-05-28 15:50 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Jeremy Spewock, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Thu, May 9, 2024 at 7:21 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> The way nodes and interactive shells interact makes it difficult to
> develop for static type checking and hinting. The current system relies
> on a top-down approach, attempting to give a generic interface to the
> test developer, hiding the interaction of concrete shell classes as much
> as possible. When working with strong typing this approach is not ideal,
> as Python's implementation of generics is still rudimentary.
>
> This rework reverses the tests interaction to a bottom-up approach,
> allowing the test developer to call concrete shell classes directly,
> and let them ingest nodes independently. While also re-enforcing type
> checking and making the code easier to read.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>  dts/framework/params/eal.py                   |   6 +-
>  dts/framework/remote_session/dpdk_shell.py    | 104 ++++++++++++++++
>  .../remote_session/interactive_shell.py       |  75 +++++++-----
>  dts/framework/remote_session/python_shell.py  |   4 +-
>  dts/framework/remote_session/testpmd_shell.py |  64 +++++-----
>  dts/framework/testbed_model/node.py           |  36 +-----
>  dts/framework/testbed_model/os_session.py     |  36 +-----
>  dts/framework/testbed_model/sut_node.py       | 112 +-----------------
>  .../testbed_model/traffic_generator/scapy.py  |   4 +-
>  dts/tests/TestSuite_hello_world.py            |   7 +-
>  dts/tests/TestSuite_pmd_buffer_scatter.py     |  21 ++--
>  dts/tests/TestSuite_smoke_tests.py            |   2 +-
>  12 files changed, 201 insertions(+), 270 deletions(-)
>  create mode 100644 dts/framework/remote_session/dpdk_shell.py
>
> diff --git a/dts/framework/params/eal.py b/dts/framework/params/eal.py
> index bbdbc8f334..8d7766fefc 100644
> --- a/dts/framework/params/eal.py
> +++ b/dts/framework/params/eal.py
> @@ -35,9 +35,9 @@ class EalParams(Params):
>                  ``other_eal_param='--single-file-segments'``
>      """
>
> -    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
> -    memory_channels: int = field(metadata=Params.short("n"))
> -    prefix: str = field(metadata=Params.long("file-prefix"))
> +    lcore_list: LogicalCoreList | None = field(default=None, metadata=Params.short("l"))
> +    memory_channels: int | None = field(default=None, metadata=Params.short("n"))
> +    prefix: str = field(default="dpdk", metadata=Params.long("file-prefix"))
>      no_pci: Switch = None
>      vdevs: list[VirtualDevice] | None = field(
>          default=None, metadata=Params.multiple() | Params.long("vdev")
> diff --git a/dts/framework/remote_session/dpdk_shell.py b/dts/framework/remote_session/dpdk_shell.py
> new file mode 100644
> index 0000000000..78caae36ea
> --- /dev/null
> +++ b/dts/framework/remote_session/dpdk_shell.py
> @@ -0,0 +1,104 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2024 Arm Limited
> +
> +"""DPDK-based interactive shell.
> +
> +Provides a base class to create interactive shells based on DPDK.
> +"""
> +
> +
> +from abc import ABC
> +
> +from framework.params.eal import EalParams
> +from framework.remote_session.interactive_shell import InteractiveShell
> +from framework.settings import SETTINGS
> +from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
> +from framework.testbed_model.sut_node import SutNode
> +
> +
> +def compute_eal_params(
> +    node: SutNode,
> +    params: EalParams | None = None,
> +    lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
> +    ascending_cores: bool = True,
> +    append_prefix_timestamp: bool = True,
> +) -> EalParams:
> +    """Compute EAL parameters based on the node's specifications.
> +
> +    Args:
> +        node: The SUT node to compute the values for.
> +        params: The EalParams object to amend, if set to None a new object is created and returned.
> +        lcore_filter_specifier: A number of lcores/cores/sockets to use
> +            or a list of lcore ids to use.
> +            The default will select one lcore for each of two cores
> +            on one socket, in ascending order of core ids.
> +        ascending_cores: Sort cores in ascending order (lowest to highest IDs).
> +            If :data:`False`, sort in descending order.
> +        append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.
> +    """
> +    if params is None:
> +        params = EalParams()
> +
> +    if params.lcore_list is None:
> +        params.lcore_list = LogicalCoreList(
> +            node.filter_lcores(lcore_filter_specifier, ascending_cores)
> +        )
> +
> +    prefix = params.prefix
> +    if append_prefix_timestamp:
> +        prefix = f"{prefix}_{node._dpdk_timestamp}"
> +    prefix = node.main_session.get_dpdk_file_prefix(prefix)
> +    if prefix:
> +        node._dpdk_prefix_list.append(prefix)
> +    params.prefix = prefix
> +
> +    if params.ports is None:
> +        params.ports = node.ports
> +
> +    return params
> +
> +
> +class DPDKShell(InteractiveShell, ABC):
> +    """The base class for managing DPDK-based interactive shells.
> +
> +    This class shouldn't be instantiated directly, but instead be extended.
> +    It automatically injects computed EAL parameters based on the node in the
> +    supplied app parameters.
> +    """
> +
> +    _node: SutNode
> +    _app_params: EalParams
> +
> +    _lcore_filter_specifier: LogicalCoreCount | LogicalCoreList
> +    _ascending_cores: bool
> +    _append_prefix_timestamp: bool
> +
> +    def __init__(
> +        self,
> +        node: SutNode,
> +        app_params: EalParams,
> +        privileged: bool = True,
> +        timeout: float = SETTINGS.timeout,
> +        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
> +        ascending_cores: bool = True,
> +        append_prefix_timestamp: bool = True,
> +        start_on_init: bool = True,
> +    ) -> None:
> +        """Overrides :meth:`~.interactive_shell.InteractiveShell.__init__`."""
> +        self._lcore_filter_specifier = lcore_filter_specifier
> +        self._ascending_cores = ascending_cores
> +        self._append_prefix_timestamp = append_prefix_timestamp
> +
> +        super().__init__(node, app_params, privileged, timeout, start_on_init)
> +
> +    def __post_init__(self):
> +        """Computes EAL params based on the node capabilities before start."""
> +        self._app_params = compute_eal_params(
> +            self._node,
> +            self._app_params,
> +            self._lcore_filter_specifier,
> +            self._ascending_cores,
> +            self._append_prefix_timestamp,
> +        )
> +
> +        self._update_path(self._node.remote_dpdk_build_dir.joinpath(self.path))
> diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
> index 9da66d1c7e..8163c8f247 100644
> --- a/dts/framework/remote_session/interactive_shell.py
> +++ b/dts/framework/remote_session/interactive_shell.py
> @@ -17,13 +17,14 @@
>
>  from abc import ABC
>  from pathlib import PurePath
> -from typing import Callable, ClassVar
> +from typing import ClassVar
>
> -from paramiko import Channel, SSHClient, channel  # type: ignore[import-untyped]
> +from paramiko import Channel, channel  # type: ignore[import-untyped]
>
>  from framework.logger import DTSLogger
>  from framework.params import Params
>  from framework.settings import SETTINGS
> +from framework.testbed_model.node import Node
>
>
>  class InteractiveShell(ABC):
> @@ -36,13 +37,14 @@ class InteractiveShell(ABC):
>      session.
>      """
>
> -    _interactive_session: SSHClient
> +    _node: Node
>      _stdin: channel.ChannelStdinFile
>      _stdout: channel.ChannelFile
>      _ssh_channel: Channel
>      _logger: DTSLogger
>      _timeout: float
>      _app_params: Params
> +    _privileged: bool
>
>      #: Prompt to expect at the end of output when sending a command.
>      #: This is often overridden by subclasses.
> @@ -56,57 +58,66 @@ class InteractiveShell(ABC):
>      #: Path to the executable to start the interactive application.
>      path: ClassVar[PurePath]
>
> -    #: Whether this application is a DPDK app. If it is, the build directory
> -    #: for DPDK on the node will be prepended to the path to the executable.
> -    dpdk_app: ClassVar[bool] = False
> -
>      def __init__(
>          self,
> -        interactive_session: SSHClient,
> -        logger: DTSLogger,
> -        get_privileged_command: Callable[[str], str] | None,
> +        node: Node,
>          app_params: Params = Params(),
> +        privileged: bool = False,
>          timeout: float = SETTINGS.timeout,
> +        start_on_init: bool = True,
>      ) -> None:
>          """Create an SSH channel during initialization.
>
>          Args:
> -            interactive_session: The SSH session dedicated to interactive shells.
> -            logger: The logger instance this session will use.
> -            get_privileged_command: A method for modifying a command to allow it to use
> -                elevated privileges. If :data:`None`, the application will not be started
> -                with elevated privileges.
> +            node: The node on which to run start the interactive shell.
>              app_params: The command line parameters to be passed to the application on startup.
> +            privileged: Enables the shell to run as superuser.
>              timeout: The timeout used for the SSH channel that is dedicated to this interactive
>                  shell. This timeout is for collecting output, so if reading from the buffer
>                  and no output is gathered within the timeout, an exception is thrown.
> +            start_on_init: Start interactive shell automatically after object initialisation.
>          """
> -        self._interactive_session = interactive_session
> -        self._ssh_channel = self._interactive_session.invoke_shell()
> +        self._node = node
> +        self._logger = node._logger
> +        self._app_params = app_params
> +        self._privileged = privileged
> +        self._timeout = timeout
> +        # Ensure path is properly formatted for the host
> +        self._update_path(self._node.main_session.join_remote_path(self.path))
> +
> +        self.__post_init__()
> +
> +        if start_on_init:
> +            self.start_application()
> +
> +    def __post_init__(self):
> +        """Overridable. Method called after the object init and before application start."""
> +        pass
> +
> +    def _setup_ssh_channel(self):
> +        self._ssh_channel = self._node.main_session.interactive_session.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.settimeout(self._timeout)
>          self._ssh_channel.set_combine_stderr(True)  # combines stdout and stderr streams
> -        self._logger = logger
> -        self._timeout = timeout
> -        self._app_params = app_params
> -        self._start_application(get_privileged_command)
>
> -    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
> +    def start_application(self) -> None:
>          """Starts a new interactive application based on the path to the app.
>
>          This method is often overridden by subclasses as their process for
>          starting may look different.
> -
> -        Args:
> -            get_privileged_command: A function (but could be any callable) that produces
> -                the version of the command with elevated privileges.
>          """
> -        start_command = f"{self.path} {self._app_params}"
> -        if get_privileged_command is not None:
> -            start_command = get_privileged_command(start_command)
> +        self._setup_ssh_channel()
> +
> +        start_command = self._make_start_command()
> +        if self._privileged:
> +            start_command = self._node.main_session._get_privileged_command(start_command)
>          self.send_command(start_command)
>
> +    def _make_start_command(self) -> str:
> +        """Makes the command that starts the interactive shell."""
> +        return f"{self.path} {self._app_params or ''}"
> +
>      def send_command(self, command: str, prompt: str | None = None) -> str:
>          """Send `command` and get all output before the expected ending string.
>
> @@ -150,3 +161,7 @@ def close(self) -> None:
>      def __del__(self) -> None:
>          """Make sure the session is properly closed before deleting the object."""
>          self.close()
> +
> +    @classmethod
> +    def _update_path(cls, path: PurePath) -> None:
> +        cls.path = path
> diff --git a/dts/framework/remote_session/python_shell.py b/dts/framework/remote_session/python_shell.py
> index ccfd3783e8..953ed100df 100644
> --- a/dts/framework/remote_session/python_shell.py
> +++ b/dts/framework/remote_session/python_shell.py
> @@ -6,9 +6,7 @@
>  Typical usage example in a TestSuite::
>
>      from framework.remote_session import PythonShell
> -    python_shell = self.tg_node.create_interactive_shell(
> -        PythonShell, timeout=5, privileged=True
> -    )
> +    python_shell = PythonShell(self.tg_node, timeout=5, privileged=True)
>      python_shell.send_command("print('Hello World')")
>      python_shell.close()
>  """
> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
> index ef3f23c582..92930d7fbb 100644
> --- a/dts/framework/remote_session/testpmd_shell.py
> +++ b/dts/framework/remote_session/testpmd_shell.py
> @@ -7,9 +7,7 @@
>
>  Typical usage example in a TestSuite::
>
> -    testpmd_shell = self.sut_node.create_interactive_shell(
> -            TestPmdShell, privileged=True
> -        )
> +    testpmd_shell = TestPmdShell()
>      devices = testpmd_shell.get_devices()
>      for device in devices:
>          print(device)
> @@ -18,13 +16,14 @@
>
>  import time
>  from pathlib import PurePath
> -from typing import Callable, ClassVar
> +from typing import ClassVar
>
>  from framework.exception import InteractiveCommandExecutionError
>  from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
> +from framework.remote_session.dpdk_shell import DPDKShell
>  from framework.settings import SETTINGS
> -
> -from .interactive_shell import InteractiveShell
> +from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
> +from framework.testbed_model.sut_node import SutNode
>
>
>  class TestPmdDevice(object):
> @@ -49,52 +48,48 @@ def __str__(self) -> str:
>          return self.pci_address
>
>
> -class TestPmdShell(InteractiveShell):
> +class TestPmdShell(DPDKShell):
>      """Testpmd interactive shell.
>
>      The testpmd shell users should never use
>      the :meth:`~.interactive_shell.InteractiveShell.send_command` method directly, but rather
>      call specialized methods. If there isn't one that satisfies a need, it should be added.
> -
> -    Attributes:
> -        number_of_ports: The number of ports which were allowed on the command-line when testpmd
> -            was started.
>      """
>
> -    number_of_ports: int
> +    _app_params: TestPmdParams
>
>      #: The path to the testpmd executable.
>      path: ClassVar[PurePath] = PurePath("app", "dpdk-testpmd")
>
> -    #: Flag this as a DPDK app so that it's clear this is not a system app and
> -    #: needs to be looked in a specific path.
> -    dpdk_app: ClassVar[bool] = True
> -
>      #: The testpmd's prompt.
>      _default_prompt: ClassVar[str] = "testpmd>"
>
>      #: This forces the prompt to appear after sending a command.
>      _command_extra_chars: ClassVar[str] = "\n"
>
> -    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
> -        """Overrides :meth:`~.interactive_shell._start_application`.
> -
> -        Add flags for starting testpmd in interactive mode and disabling messages for link state
> -        change events before starting the application. Link state is verified before starting
> -        packet forwarding and the messages create unexpected newlines in the terminal which
> -        complicates output collection.
> -
> -        Also find the number of pci addresses which were allowed on the command line when the app
> -        was started.
> -        """
> -        assert isinstance(self._app_params, TestPmdParams)
> -
> -        self.number_of_ports = (
> -            len(self._app_params.ports) if self._app_params.ports is not None else 0
> +    def __init__(
> +        self,
> +        node: SutNode,
> +        privileged: bool = True,
> +        timeout: float = SETTINGS.timeout,
> +        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
> +        ascending_cores: bool = True,
> +        append_prefix_timestamp: bool = True,
> +        start_on_init: bool = True,
> +        **app_params,
> +    ) -> None:
> +        """Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs."""
> +        super().__init__(
> +            node,
> +            TestPmdParams(**app_params),
> +            privileged,
> +            timeout,
> +            lcore_filter_specifier,
> +            ascending_cores,
> +            append_prefix_timestamp,
> +            start_on_init,
>          )
>
> -        super()._start_application(get_privileged_command)
> -
>      def start(self, verify: bool = True) -> None:
>          """Start packet forwarding with the current configuration.
>
> @@ -114,7 +109,8 @@ def start(self, verify: bool = True) -> None:
>                  self._logger.debug(f"Failed to start packet forwarding: \n{start_cmd_output}")
>                  raise InteractiveCommandExecutionError("Testpmd failed to start packet forwarding.")
>
> -            for port_id in range(self.number_of_ports):
> +            number_of_ports = len(self._app_params.ports or [])
> +            for port_id in range(number_of_ports):
>                  if not self.wait_link_status_up(port_id):
>                      raise InteractiveCommandExecutionError(
>                          "Not all ports came up after starting packet forwarding in testpmd."
> diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
> index 6af4f25a3c..88395faabe 100644
> --- a/dts/framework/testbed_model/node.py
> +++ b/dts/framework/testbed_model/node.py
> @@ -15,7 +15,7 @@
>
>  from abc import ABC
>  from ipaddress import IPv4Interface, IPv6Interface
> -from typing import Any, Callable, Type, Union
> +from typing import Any, Callable, Union
>
>  from framework.config import (
>      OS,
> @@ -25,7 +25,6 @@
>  )
>  from framework.exception import ConfigurationError
>  from framework.logger import DTSLogger, get_dts_logger
> -from framework.params import Params
>  from framework.settings import SETTINGS
>
>  from .cpu import (
> @@ -36,7 +35,7 @@
>      lcore_filter,
>  )
>  from .linux_session import LinuxSession
> -from .os_session import InteractiveShellType, OSSession
> +from .os_session import OSSession
>  from .port import Port
>  from .virtual_device import VirtualDevice
>
> @@ -196,37 +195,6 @@ def create_session(self, name: str) -> OSSession:
>          self._other_sessions.append(connection)
>          return connection
>
> -    def create_interactive_shell(
> -        self,
> -        shell_cls: Type[InteractiveShellType],
> -        timeout: float = SETTINGS.timeout,
> -        privileged: bool = False,
> -        app_params: Params = Params(),
> -    ) -> InteractiveShellType:
> -        """Factory for interactive session handlers.
> -
> -        Instantiate `shell_cls` according to the remote OS specifics.
> -
> -        Args:
> -            shell_cls: The class of the shell.
> -            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.
> -            privileged: Whether to run the shell with administrative privileges.
> -            app_args: The arguments to be passed to the application.
> -
> -        Returns:
> -            An instance of the desired interactive application shell.
> -        """
> -        if not shell_cls.dpdk_app:
> -            shell_cls.path = self.main_session.join_remote_path(shell_cls.path)
> -
> -        return self.main_session.create_interactive_shell(
> -            shell_cls,
> -            timeout,
> -            privileged,
> -            app_params,
> -        )
> -
>      def filter_lcores(
>          self,
>          filter_specifier: LogicalCoreCount | LogicalCoreList,
> diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
> index e5f5fcbe0e..e7e6c9d670 100644
> --- a/dts/framework/testbed_model/os_session.py
> +++ b/dts/framework/testbed_model/os_session.py
> @@ -26,18 +26,16 @@
>  from collections.abc import Iterable
>  from ipaddress import IPv4Interface, IPv6Interface
>  from pathlib import PurePath
> -from typing import Type, TypeVar, Union
> +from typing import Union
>
>  from framework.config import Architecture, NodeConfiguration, NodeInfo
>  from framework.logger import DTSLogger
> -from framework.params import Params
>  from framework.remote_session import (
>      InteractiveRemoteSession,
>      RemoteSession,
>      create_interactive_session,
>      create_remote_session,
>  )
> -from framework.remote_session.interactive_shell import InteractiveShell
>  from framework.remote_session.remote_session import CommandResult
>  from framework.settings import SETTINGS
>  from framework.utils import MesonArgs
> @@ -45,8 +43,6 @@
>  from .cpu import LogicalCore
>  from .port import Port
>
> -InteractiveShellType = TypeVar("InteractiveShellType", bound=InteractiveShell)
> -
>
>  class OSSession(ABC):
>      """OS-unaware to OS-aware translation API definition.
> @@ -131,36 +127,6 @@ def send_command(
>
>          return self.remote_session.send_command(command, timeout, verify, env)
>
> -    def create_interactive_shell(
> -        self,
> -        shell_cls: Type[InteractiveShellType],
> -        timeout: float,
> -        privileged: bool,
> -        app_args: Params,
> -    ) -> InteractiveShellType:
> -        """Factory for interactive session handlers.
> -
> -        Instantiate `shell_cls` according to the remote OS specifics.
> -
> -        Args:
> -            shell_cls: The class of the shell.
> -            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.
> -            privileged: Whether to run the shell with administrative privileges.
> -            app_args: The arguments to be passed to the application.
> -
> -        Returns:
> -            An instance of the desired interactive application shell.
> -        """
> -        return shell_cls(
> -            self.interactive_session.session,
> -            self._logger,
> -            self._get_privileged_command if privileged else None,
> -            app_args,
> -            timeout,
> -        )
> -
>      @staticmethod
>      @abstractmethod
>      def _get_privileged_command(command: str) -> str:
> diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
> index 83ad06ae2d..727170b7fc 100644
> --- a/dts/framework/testbed_model/sut_node.py
> +++ b/dts/framework/testbed_model/sut_node.py
> @@ -16,7 +16,6 @@
>  import tarfile
>  import time
>  from pathlib import PurePath
> -from typing import Type
>
>  from framework.config import (
>      BuildTargetConfiguration,
> @@ -24,17 +23,13 @@
>      NodeInfo,
>      SutNodeConfiguration,
>  )
> -from framework.params import Params, Switch
>  from framework.params.eal import EalParams
>  from framework.remote_session.remote_session import CommandResult
>  from framework.settings import SETTINGS
>  from framework.utils import MesonArgs
>
> -from .cpu import LogicalCoreCount, LogicalCoreList
>  from .node import Node
> -from .os_session import InteractiveShellType, OSSession
> -from .port import Port
> -from .virtual_device import VirtualDevice
> +from .os_session import OSSession
>
>
>  class SutNode(Node):
> @@ -289,68 +284,6 @@ def kill_cleanup_dpdk_apps(self) -> None:
>              self._dpdk_kill_session = self.create_session("dpdk_kill")
>          self._dpdk_prefix_list = []
>
> -    def create_eal_parameters(
> -        self,
> -        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
> -        ascending_cores: bool = True,
> -        prefix: str = "dpdk",
> -        append_prefix_timestamp: bool = True,
> -        no_pci: Switch = None,
> -        vdevs: list[VirtualDevice] | None = None,
> -        ports: list[Port] | None = None,
> -        other_eal_param: str = "",
> -    ) -> EalParams:
> -        """Compose the EAL parameters.
> -
> -        Process the list of cores and the DPDK prefix and pass that along with
> -        the rest of the arguments.
> -
> -        Args:
> -            lcore_filter_specifier: A number of lcores/cores/sockets to use
> -                or a list of lcore ids to use.
> -                The default will select one lcore for each of two cores
> -                on one socket, in ascending order of core ids.
> -            ascending_cores: Sort cores in ascending order (lowest to highest IDs).
> -                If :data:`False`, sort in descending order.
> -            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
> -            append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.
> -            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
> -            vdevs: Virtual devices, e.g.::
> -
> -                vdevs=[
> -                    VirtualDevice('net_ring0'),
> -                    VirtualDevice('net_ring1')
> -                ]
> -            ports: The list of ports to allow. If :data:`None`, all ports listed in `self.ports`
> -                will be allowed.
> -            other_eal_param: user defined DPDK EAL parameters, e.g.:
> -                ``other_eal_param='--single-file-segments'``.
> -
> -        Returns:
> -            An EAL param string, such as
> -            ``-c 0xf -a 0000:88:00.0 --file-prefix=dpdk_1112_20190809143420``.
> -        """
> -        lcore_list = LogicalCoreList(self.filter_lcores(lcore_filter_specifier, ascending_cores))
> -
> -        if append_prefix_timestamp:
> -            prefix = f"{prefix}_{self._dpdk_timestamp}"
> -        prefix = self.main_session.get_dpdk_file_prefix(prefix)
> -        if prefix:
> -            self._dpdk_prefix_list.append(prefix)
> -
> -        if ports is None:
> -            ports = self.ports
> -
> -        return EalParams(
> -            lcore_list=lcore_list,
> -            memory_channels=self.config.memory_channels,
> -            prefix=prefix,
> -            no_pci=no_pci,
> -            vdevs=vdevs,
> -            ports=ports,
> -            other_eal_param=Params.from_str(other_eal_param),
> -        )
> -
>      def run_dpdk_app(
>          self, app_path: PurePath, eal_params: EalParams, timeout: float = 30
>      ) -> CommandResult:
> @@ -379,49 +312,6 @@ def configure_ipv4_forwarding(self, enable: bool) -> None:
>          """
>          self.main_session.configure_ipv4_forwarding(enable)
>
> -    def create_interactive_shell(
> -        self,
> -        shell_cls: Type[InteractiveShellType],
> -        timeout: float = SETTINGS.timeout,
> -        privileged: bool = False,
> -        app_params: Params = Params(),
> -        eal_params: EalParams | None = None,
> -    ) -> InteractiveShellType:
> -        """Extend the factory for interactive session handlers.
> -
> -        The extensions are SUT node specific:
> -
> -            * The default for `eal_parameters`,
> -            * The interactive shell path `shell_cls.path` is prepended with path to the remote
> -              DPDK build directory for DPDK apps.
> -
> -        Args:
> -            shell_cls: The class of the shell.
> -            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.
> -            privileged: Whether to run the shell with administrative privileges.
> -            app_params: The parameters to be passed to the application.
> -            eal_params: List of EAL parameters to use to launch the app. If this
> -                isn't provided or an empty string is passed, it will default to calling
> -                :meth:`create_eal_parameters`.
> -
> -        Returns:
> -            An instance of the desired interactive application shell.
> -        """
> -        # We need to append the build directory and add EAL parameters for DPDK apps
> -        if shell_cls.dpdk_app:
> -            if eal_params is None:
> -                eal_params = self.create_eal_parameters()
> -            eal_params.append_str(str(app_params))
> -            app_params = eal_params
> -
> -            shell_cls.path = self.main_session.join_remote_path(
> -                self.remote_dpdk_build_dir, shell_cls.path
> -            )
> -
> -        return super().create_interactive_shell(shell_cls, timeout, privileged, app_params)
> -
>      def bind_ports_to_driver(self, for_dpdk: bool = True) -> None:
>          """Bind all ports on the SUT to a driver.
>
> diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py
> index 7bc1c2cc08..bf58ad1c5e 100644
> --- a/dts/framework/testbed_model/traffic_generator/scapy.py
> +++ b/dts/framework/testbed_model/traffic_generator/scapy.py
> @@ -217,9 +217,7 @@ def __init__(self, tg_node: Node, config: ScapyTrafficGeneratorConfig):
>              self._tg_node.config.os == OS.linux
>          ), "Linux is the only supported OS for scapy traffic generation"
>
> -        self.session = self._tg_node.create_interactive_shell(
> -            PythonShell, timeout=5, privileged=True
> -        )
> +        self.session = PythonShell(self._tg_node, timeout=5, privileged=True)
>
>          # import libs in remote python console
>          for import_statement in SCAPY_RPC_SERVER_IMPORTS:
> diff --git a/dts/tests/TestSuite_hello_world.py b/dts/tests/TestSuite_hello_world.py
> index 0d6995f260..d958f99030 100644
> --- a/dts/tests/TestSuite_hello_world.py
> +++ b/dts/tests/TestSuite_hello_world.py
> @@ -7,6 +7,7 @@
>  No other EAL parameters apart from cores are used.
>  """
>
> +from framework.remote_session.dpdk_shell import compute_eal_params
>  from framework.test_suite import TestSuite
>  from framework.testbed_model.cpu import (
>      LogicalCoreCount,
> @@ -38,7 +39,7 @@ def test_hello_world_single_core(self) -> None:
>          # get the first usable core
>          lcore_amount = LogicalCoreCount(1, 1, 1)
>          lcores = LogicalCoreCountFilter(self.sut_node.lcores, lcore_amount).filter()
> -        eal_para = self.sut_node.create_eal_parameters(lcore_filter_specifier=lcore_amount)
> +        eal_para = compute_eal_params(self.sut_node, lcore_filter_specifier=lcore_amount)
>          result = self.sut_node.run_dpdk_app(self.app_helloworld_path, eal_para)
>          self.verify(
>              f"hello from core {int(lcores[0])}" in result.stdout,
> @@ -55,8 +56,8 @@ def test_hello_world_all_cores(self) -> None:
>              "hello from core <core_id>"
>          """
>          # get the maximum logical core number
> -        eal_para = self.sut_node.create_eal_parameters(
> -            lcore_filter_specifier=LogicalCoreList(self.sut_node.lcores)
> +        eal_para = compute_eal_params(
> +            self.sut_node, lcore_filter_specifier=LogicalCoreList(self.sut_node.lcores)
>          )
>          result = self.sut_node.run_dpdk_app(self.app_helloworld_path, eal_para, 50)
>          for lcore in self.sut_node.lcores:
> diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
> index 6d206c1a40..43cf5c61eb 100644
> --- a/dts/tests/TestSuite_pmd_buffer_scatter.py
> +++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
> @@ -16,14 +16,13 @@
>  """
>
>  import struct
> -from dataclasses import asdict
>
>  from scapy.layers.inet import IP  # type: ignore[import-untyped]
>  from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
>  from scapy.packet import Raw  # type: ignore[import-untyped]
>  from scapy.utils import hexstr  # type: ignore[import-untyped]
>
> -from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
> +from framework.params.testpmd import SimpleForwardingModes
>  from framework.remote_session.testpmd_shell import TestPmdShell
>  from framework.test_suite import TestSuite
>
> @@ -103,17 +102,13 @@ def pmd_scatter(self, mbsize: int) -> None:
>          Test:
>              Start testpmd and run functional test with preset mbsize.
>          """
> -        testpmd = self.sut_node.create_interactive_shell(
> -            TestPmdShell,
> -            app_params=TestPmdParams(
> -                forward_mode=SimpleForwardingModes.mac,
> -                mbcache=200,
> -                mbuf_size=[mbsize],
> -                max_pkt_len=9000,
> -                tx_offloads=0x00008000,
> -                **asdict(self.sut_node.create_eal_parameters()),
> -            ),
> -            privileged=True,
> +        testpmd = TestPmdShell(
> +            self.sut_node,
> +            forward_mode=SimpleForwardingModes.mac,
> +            mbcache=200,
> +            mbuf_size=[mbsize],
> +            max_pkt_len=9000,
> +            tx_offloads=0x00008000,
>          )
>          testpmd.start()
>
> diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py
> index ca678f662d..eca27acfd8 100644
> --- a/dts/tests/TestSuite_smoke_tests.py
> +++ b/dts/tests/TestSuite_smoke_tests.py
> @@ -99,7 +99,7 @@ def test_devices_listed_in_testpmd(self) -> None:
>          Test:
>              List all devices found in testpmd and verify the configured devices are among them.
>          """
> -        testpmd_driver = self.sut_node.create_interactive_shell(TestPmdShell, privileged=True)
> +        testpmd_driver = TestPmdShell(self.sut_node)
>          dev_list = [str(x) for x in testpmd_driver.get_devices()]
>          for nic in self.nics_in_node:
>              self.verify(
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 8/8] dts: use Unpack for type checking and hinting
  2024-05-09 11:20   ` [PATCH v2 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
@ 2024-05-28 15:50     ` Nicholas Pratte
  2024-05-28 21:08     ` Jeremy Spewock
  1 sibling, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-05-28 15:50 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Jeremy Spewock, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Thu, May 9, 2024 at 7:21 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Interactive shells that inherit DPDKShell initialise their params
> classes from a kwargs dict. Therefore, static type checking is
> disabled. This change uses the functionality of Unpack added in
> PEP 692 to re-enable it. The disadvantage is that this functionality has
> been implemented only with TypedDict, forcing the creation of TypedDict
> mirrors of the Params classes.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>  dts/framework/params/types.py                 | 133 ++++++++++++++++++
>  dts/framework/remote_session/testpmd_shell.py |   5 +-
>  2 files changed, 137 insertions(+), 1 deletion(-)
>  create mode 100644 dts/framework/params/types.py
>
> diff --git a/dts/framework/params/types.py b/dts/framework/params/types.py
> new file mode 100644
> index 0000000000..e668f658d8
> --- /dev/null
> +++ b/dts/framework/params/types.py
> @@ -0,0 +1,133 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2024 Arm Limited
> +
> +"""Module containing TypeDict-equivalents of Params classes for static typing and hinting.
> +
> +TypedDicts can be used in conjunction with Unpack and kwargs for type hinting on function calls.
> +
> +Example:
> +    ..code:: python
> +        def create_testpmd(**kwargs: Unpack[TestPmdParamsDict]):
> +            params = TestPmdParams(**kwargs)
> +"""
> +
> +from pathlib import PurePath
> +from typing import TypedDict
> +
> +from framework.params import Switch, YesNoSwitch
> +from framework.params.testpmd import (
> +    AnonMempoolAllocationMode,
> +    EthPeer,
> +    Event,
> +    FlowGenForwardingMode,
> +    HairpinMode,
> +    NoisyForwardingMode,
> +    Params,
> +    PortNUMAConfig,
> +    PortTopology,
> +    RingNUMAConfig,
> +    RSSSetting,
> +    RXMultiQueueMode,
> +    RXRingParams,
> +    SimpleForwardingModes,
> +    SimpleMempoolAllocationMode,
> +    TxIPAddrPair,
> +    TXOnlyForwardingMode,
> +    TXRingParams,
> +    TxUDPPortPair,
> +)
> +from framework.testbed_model.cpu import LogicalCoreList
> +from framework.testbed_model.port import Port
> +from framework.testbed_model.virtual_device import VirtualDevice
> +
> +
> +class EalParamsDict(TypedDict, total=False):
> +    """:class:`TypedDict` equivalent of :class:`~.eal.EalParams`."""
> +
> +    lcore_list: LogicalCoreList | None
> +    memory_channels: int | None
> +    prefix: str
> +    no_pci: Switch
> +    vdevs: list[VirtualDevice] | None
> +    ports: list[Port] | None
> +    other_eal_param: Params | None
> +
> +
> +class TestPmdParamsDict(EalParamsDict, total=False):
> +    """:class:`TypedDict` equivalent of :class:`~.testpmd.TestPmdParams`."""
> +
> +    interactive_mode: Switch
> +    auto_start: Switch
> +    tx_first: Switch
> +    stats_period: int | None
> +    display_xstats: list[str] | None
> +    nb_cores: int | None
> +    coremask: int | None
> +    nb_ports: int | None
> +    port_topology: PortTopology | None
> +    portmask: int | None
> +    portlist: str | None
> +    numa: YesNoSwitch
> +    socket_num: int | None
> +    port_numa_config: list[PortNUMAConfig] | None
> +    ring_numa_config: list[RingNUMAConfig] | None
> +    total_num_mbufs: int | None
> +    mbuf_size: list[int] | None
> +    mbcache: int | None
> +    max_pkt_len: int | None
> +    eth_peers_configfile: PurePath | None
> +    eth_peer: list[EthPeer] | None
> +    tx_ip: TxIPAddrPair | None
> +    tx_udp: TxUDPPortPair | None
> +    enable_lro: Switch
> +    max_lro_pkt_size: int | None
> +    disable_crc_strip: Switch
> +    enable_scatter: Switch
> +    enable_hw_vlan: Switch
> +    enable_hw_vlan_filter: Switch
> +    enable_hw_vlan_strip: Switch
> +    enable_hw_vlan_extend: Switch
> +    enable_hw_qinq_strip: Switch
> +    pkt_drop_enabled: Switch
> +    rss: RSSSetting | None
> +    forward_mode: (
> +        SimpleForwardingModes
> +        | FlowGenForwardingMode
> +        | TXOnlyForwardingMode
> +        | NoisyForwardingMode
> +        | None
> +    )
> +    hairpin_mode: HairpinMode | None
> +    hairpin_queues: int | None
> +    burst: int | None
> +    enable_rx_cksum: Switch
> +    rx_queues: int | None
> +    rx_ring: RXRingParams | None
> +    no_flush_rx: Switch
> +    rx_segments_offsets: list[int] | None
> +    rx_segments_length: list[int] | None
> +    multi_rx_mempool: Switch
> +    rx_shared_queue: Switch | int
> +    rx_offloads: int | None
> +    rx_mq_mode: RXMultiQueueMode | None
> +    tx_queues: int | None
> +    tx_ring: TXRingParams | None
> +    tx_offloads: int | None
> +    eth_link_speed: int | None
> +    disable_link_check: Switch
> +    disable_device_start: Switch
> +    no_lsc_interrupt: Switch
> +    no_rmv_interrupt: Switch
> +    bitrate_stats: int | None
> +    latencystats: int | None
> +    print_events: list[Event] | None
> +    mask_events: list[Event] | None
> +    flow_isolate_all: Switch
> +    disable_flow_flush: Switch
> +    hot_plug: Switch
> +    vxlan_gpe_port: int | None
> +    geneve_parsed_port: int | None
> +    lock_all_memory: YesNoSwitch
> +    mempool_allocation_mode: SimpleMempoolAllocationMode | AnonMempoolAllocationMode | None
> +    record_core_cycles: Switch
> +    record_burst_status: Switch
> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
> index 92930d7fbb..5b3a7bb9ab 100644
> --- a/dts/framework/remote_session/testpmd_shell.py
> +++ b/dts/framework/remote_session/testpmd_shell.py
> @@ -18,8 +18,11 @@
>  from pathlib import PurePath
>  from typing import ClassVar
>
> +from typing_extensions import Unpack
> +
>  from framework.exception import InteractiveCommandExecutionError
>  from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
> +from framework.params.types import TestPmdParamsDict
>  from framework.remote_session.dpdk_shell import DPDKShell
>  from framework.settings import SETTINGS
>  from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
> @@ -76,7 +79,7 @@ def __init__(
>          ascending_cores: bool = True,
>          append_prefix_timestamp: bool = True,
>          start_on_init: bool = True,
> -        **app_params,
> +        **app_params: Unpack[TestPmdParamsDict],
>      ) -> None:
>          """Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs."""
>          super().__init__(
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 5/8] dts: add testpmd shell params
  2024-05-09 11:20   ` [PATCH v2 5/8] dts: add testpmd shell params Luca Vizzarro
@ 2024-05-28 15:53     ` Nicholas Pratte
  2024-05-28 21:05     ` Jeremy Spewock
  1 sibling, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-05-28 15:53 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Jeremy Spewock, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>


On Thu, May 9, 2024 at 7:21 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Implement all the testpmd shell parameters into a data structure.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>  dts/framework/params/testpmd.py               | 608 ++++++++++++++++++
>  dts/framework/remote_session/testpmd_shell.py |  42 +-
>  dts/tests/TestSuite_pmd_buffer_scatter.py     |   5 +-
>  3 files changed, 615 insertions(+), 40 deletions(-)
>  create mode 100644 dts/framework/params/testpmd.py
>
> diff --git a/dts/framework/params/testpmd.py b/dts/framework/params/testpmd.py
> new file mode 100644
> index 0000000000..f8f70320cf
> --- /dev/null
> +++ b/dts/framework/params/testpmd.py
> @@ -0,0 +1,608 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2024 Arm Limited
> +
> +"""Module containing all the TestPmd-related parameter classes."""
> +
> +from dataclasses import dataclass, field
> +from enum import EnumMeta, Flag, auto, unique
> +from pathlib import PurePath
> +from typing import Literal, NamedTuple
> +
> +from framework.params import (
> +    Params,
> +    Switch,
> +    YesNoSwitch,
> +    bracketed,
> +    comma_separated,
> +    convert_str,
> +    hex_from_flag_value,
> +    str_from_flag_value,
> +)
> +from framework.params.eal import EalParams
> +from framework.utils import StrEnum
> +
> +
> +class PortTopology(StrEnum):
> +    """Enum representing the port topology."""
> +
> +    paired = auto()
> +    """In paired mode, the forwarding is between pairs of ports, e.g.: (0,1), (2,3), (4,5)."""
> +    chained = auto()
> +    """In chained mode, the forwarding is to the next available port in the port mask, e.g.:
> +    (0,1), (1,2), (2,0).
> +
> +    The ordering of the ports can be changed using the portlist testpmd runtime function.
> +    """
> +    loop = auto()
> +    """In loop mode, ingress traffic is simply transmitted back on the same interface."""
> +
> +
> +@convert_str(bracketed, comma_separated)
> +class PortNUMAConfig(NamedTuple):
> +    """DPDK port to NUMA socket association tuple."""
> +
> +    #:
> +    port: int
> +    #:
> +    socket: int
> +
> +
> +@convert_str(str_from_flag_value)
> +@unique
> +class FlowDirection(Flag):
> +    """Flag indicating the direction of the flow.
> +
> +    A bi-directional flow can be specified with the pipe:
> +
> +    >>> TestPmdFlowDirection.RX | TestPmdFlowDirection.TX
> +    <TestPmdFlowDirection.TX|RX: 3>
> +    """
> +
> +    #:
> +    RX = 1 << 0
> +    #:
> +    TX = 1 << 1
> +
> +
> +@convert_str(bracketed, comma_separated)
> +class RingNUMAConfig(NamedTuple):
> +    """Tuple associating DPDK port, direction of the flow and NUMA socket."""
> +
> +    #:
> +    port: int
> +    #:
> +    direction: FlowDirection
> +    #:
> +    socket: int
> +
> +
> +@convert_str(comma_separated)
> +class EthPeer(NamedTuple):
> +    """Tuple associating a MAC address to the specified DPDK port."""
> +
> +    #:
> +    port_no: int
> +    #:
> +    mac_address: str
> +
> +
> +@convert_str(comma_separated)
> +class TxIPAddrPair(NamedTuple):
> +    """Tuple specifying the source and destination IPs for the packets."""
> +
> +    #:
> +    source_ip: str
> +    #:
> +    dest_ip: str
> +
> +
> +@convert_str(comma_separated)
> +class TxUDPPortPair(NamedTuple):
> +    """Tuple specifying the UDP source and destination ports for the packets.
> +
> +    If leaving ``dest_port`` unspecified, ``source_port`` will be used for
> +    the destination port as well.
> +    """
> +
> +    #:
> +    source_port: int
> +    #:
> +    dest_port: int | None = None
> +
> +
> +@dataclass
> +class DisableRSS(Params):
> +    """Disables RSS (Receive Side Scaling)."""
> +
> +    _disable_rss: Literal[True] = field(
> +        default=True, init=False, metadata=Params.long("disable-rss")
> +    )
> +
> +
> +@dataclass
> +class SetRSSIPOnly(Params):
> +    """Sets RSS (Receive Side Scaling) functions for IPv4/IPv6 only."""
> +
> +    _rss_ip: Literal[True] = field(default=True, init=False, metadata=Params.long("rss-ip"))
> +
> +
> +@dataclass
> +class SetRSSUDP(Params):
> +    """Sets RSS (Receive Side Scaling) functions for IPv4/IPv6 and UDP."""
> +
> +    _rss_udp: Literal[True] = field(default=True, init=False, metadata=Params.long("rss-udp"))
> +
> +
> +class RSSSetting(EnumMeta):
> +    """Enum representing a RSS setting. Each property is a class that needs to be initialised."""
> +
> +    #:
> +    Disabled = DisableRSS
> +    #:
> +    SetIPOnly = SetRSSIPOnly
> +    #:
> +    SetUDP = SetRSSUDP
> +
> +
> +class SimpleForwardingModes(StrEnum):
> +    r"""The supported packet forwarding modes for :class:`~TestPmdShell`\s."""
> +
> +    #:
> +    io = auto()
> +    #:
> +    mac = auto()
> +    #:
> +    macswap = auto()
> +    #:
> +    rxonly = auto()
> +    #:
> +    csum = auto()
> +    #:
> +    icmpecho = auto()
> +    #:
> +    ieee1588 = auto()
> +    #:
> +    fivetswap = "5tswap"
> +    #:
> +    shared_rxq = "shared-rxq"
> +    #:
> +    recycle_mbufs = auto()
> +
> +
> +@dataclass(kw_only=True)
> +class TXOnlyForwardingMode(Params):
> +    """Sets a TX-Only forwarding mode.
> +
> +    Attributes:
> +        multi_flow: Generates multiple flows if set to True.
> +        segments_length: Sets TX segment sizes or total packet length.
> +    """
> +
> +    _forward_mode: Literal["txonly"] = field(
> +        default="txonly", init=False, metadata=Params.long("forward-mode")
> +    )
> +    multi_flow: Switch = field(default=None, metadata=Params.long("txonly-multi-flow"))
> +    segments_length: list[int] | None = field(
> +        default=None, metadata=Params.long("txpkts") | Params.convert_value(comma_separated)
> +    )
> +
> +
> +@dataclass(kw_only=True)
> +class FlowGenForwardingMode(Params):
> +    """Sets a flowgen forwarding mode.
> +
> +    Attributes:
> +        clones: Set the number of each packet clones to be sent. Sending clones reduces host CPU
> +                load on creating packets and may help in testing extreme speeds or maxing out
> +                Tx packet performance. N should be not zero, but less than ‘burst’ parameter.
> +        flows: Set the number of flows to be generated, where 1 <= N <= INT32_MAX.
> +        segments_length: Set TX segment sizes or total packet length.
> +    """
> +
> +    _forward_mode: Literal["flowgen"] = field(
> +        default="flowgen", init=False, metadata=Params.long("forward-mode")
> +    )
> +    clones: int | None = field(default=None, metadata=Params.long("flowgen-clones"))
> +    flows: int | None = field(default=None, metadata=Params.long("flowgen-flows"))
> +    segments_length: list[int] | None = field(
> +        default=None, metadata=Params.long("txpkts") | Params.convert_value(comma_separated)
> +    )
> +
> +
> +@dataclass(kw_only=True)
> +class NoisyForwardingMode(Params):
> +    """Sets a noisy forwarding mode.
> +
> +    Attributes:
> +        forward_mode: Set the noisy VNF forwarding mode.
> +        tx_sw_buffer_size: Set the maximum number of elements of the FIFO queue to be created for
> +                           buffering packets.
> +        tx_sw_buffer_flushtime: Set the time before packets in the FIFO queue are flushed.
> +        lkup_memory: Set the size of the noisy neighbor simulation memory buffer in MB to N.
> +        lkup_num_reads: Set the size of the noisy neighbor simulation memory buffer in MB to N.
> +        lkup_num_writes: Set the number of writes to be done in noisy neighbor simulation
> +                         memory buffer to N.
> +        lkup_num_reads_writes: Set the number of r/w accesses to be done in noisy neighbor
> +                               simulation memory buffer to N.
> +    """
> +
> +    _forward_mode: Literal["noisy"] = field(
> +        default="noisy", init=False, metadata=Params.long("forward-mode")
> +    )
> +    forward_mode: (
> +        Literal[
> +            SimpleForwardingModes.io,
> +            SimpleForwardingModes.mac,
> +            SimpleForwardingModes.macswap,
> +            SimpleForwardingModes.fivetswap,
> +        ]
> +        | None
> +    ) = field(default=SimpleForwardingModes.io, metadata=Params.long("noisy-forward-mode"))
> +    tx_sw_buffer_size: int | None = field(
> +        default=None, metadata=Params.long("noisy-tx-sw-buffer-size")
> +    )
> +    tx_sw_buffer_flushtime: int | None = field(
> +        default=None, metadata=Params.long("noisy-tx-sw-buffer-flushtime")
> +    )
> +    lkup_memory: int | None = field(default=None, metadata=Params.long("noisy-lkup-memory"))
> +    lkup_num_reads: int | None = field(default=None, metadata=Params.long("noisy-lkup-num-reads"))
> +    lkup_num_writes: int | None = field(default=None, metadata=Params.long("noisy-lkup-num-writes"))
> +    lkup_num_reads_writes: int | None = field(
> +        default=None, metadata=Params.long("noisy-lkup-num-reads-writes")
> +    )
> +
> +
> +@convert_str(hex_from_flag_value)
> +@unique
> +class HairpinMode(Flag):
> +    """Flag representing the hairpin mode."""
> +
> +    TWO_PORTS_LOOP = 1 << 0
> +    """Two hairpin ports loop."""
> +    TWO_PORTS_PAIRED = 1 << 1
> +    """Two hairpin ports paired."""
> +    EXPLICIT_TX_FLOW = 1 << 4
> +    """Explicit Tx flow rule."""
> +    FORCE_RX_QUEUE_MEM_SETTINGS = 1 << 8
> +    """Force memory settings of hairpin RX queue."""
> +    FORCE_TX_QUEUE_MEM_SETTINGS = 1 << 9
> +    """Force memory settings of hairpin TX queue."""
> +    RX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 12
> +    """Hairpin RX queues will use locked device memory."""
> +    RX_QUEUE_USE_RTE_MEMORY = 1 << 13
> +    """Hairpin RX queues will use RTE memory."""
> +    TX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 16
> +    """Hairpin TX queues will use locked device memory."""
> +    TX_QUEUE_USE_RTE_MEMORY = 1 << 18
> +    """Hairpin TX queues will use RTE memory."""
> +
> +
> +@dataclass(kw_only=True)
> +class RXRingParams(Params):
> +    """Sets the RX ring parameters.
> +
> +    Attributes:
> +        descriptors: Set the number of descriptors in the RX rings to N, where N > 0.
> +        prefetch_threshold: Set the prefetch threshold register of RX rings to N, where N >= 0.
> +        host_threshold: Set the host threshold register of RX rings to N, where N >= 0.
> +        write_back_threshold: Set the write-back threshold register of RX rings to N, where N >= 0.
> +        free_threshold: Set the free threshold of RX descriptors to N,
> +                        where 0 <= N < value of ``-–rxd``.
> +    """
> +
> +    descriptors: int | None = field(default=None, metadata=Params.long("rxd"))
> +    prefetch_threshold: int | None = field(default=None, metadata=Params.long("rxpt"))
> +    host_threshold: int | None = field(default=None, metadata=Params.long("rxht"))
> +    write_back_threshold: int | None = field(default=None, metadata=Params.long("rxwt"))
> +    free_threshold: int | None = field(default=None, metadata=Params.long("rxfreet"))
> +
> +
> +@convert_str(hex_from_flag_value)
> +@unique
> +class RXMultiQueueMode(Flag):
> +    """Flag representing the RX multi-queue mode."""
> +
> +    #:
> +    RSS = 1 << 0
> +    #:
> +    DCB = 1 << 1
> +    #:
> +    VMDQ = 1 << 2
> +
> +
> +@dataclass(kw_only=True)
> +class TXRingParams(Params):
> +    """Sets the TX ring parameters.
> +
> +    Attributes:
> +        descriptors: Set the number of descriptors in the TX rings to N, where N > 0.
> +        rs_bit_threshold: Set the transmit RS bit threshold of TX rings to N,
> +                          where 0 <= N <= value of ``--txd``.
> +        prefetch_threshold: Set the prefetch threshold register of TX rings to N, where N >= 0.
> +        host_threshold: Set the host threshold register of TX rings to N, where N >= 0.
> +        write_back_threshold: Set the write-back threshold register of TX rings to N, where N >= 0.
> +        free_threshold: Set the transmit free threshold of TX rings to N,
> +                        where 0 <= N <= value of ``--txd``.
> +    """
> +
> +    descriptors: int | None = field(default=None, metadata=Params.long("txd"))
> +    rs_bit_threshold: int | None = field(default=None, metadata=Params.long("txrst"))
> +    prefetch_threshold: int | None = field(default=None, metadata=Params.long("txpt"))
> +    host_threshold: int | None = field(default=None, metadata=Params.long("txht"))
> +    write_back_threshold: int | None = field(default=None, metadata=Params.long("txwt"))
> +    free_threshold: int | None = field(default=None, metadata=Params.long("txfreet"))
> +
> +
> +class Event(StrEnum):
> +    """Enum representing a testpmd event."""
> +
> +    #:
> +    unknown = auto()
> +    #:
> +    queue_state = auto()
> +    #:
> +    vf_mbox = auto()
> +    #:
> +    macsec = auto()
> +    #:
> +    intr_lsc = auto()
> +    #:
> +    intr_rmv = auto()
> +    #:
> +    intr_reset = auto()
> +    #:
> +    dev_probed = auto()
> +    #:
> +    dev_released = auto()
> +    #:
> +    flow_aged = auto()
> +    #:
> +    err_recovering = auto()
> +    #:
> +    recovery_success = auto()
> +    #:
> +    recovery_failed = auto()
> +    #:
> +    all = auto()
> +
> +
> +class SimpleMempoolAllocationMode(StrEnum):
> +    """Enum representing simple mempool allocation modes."""
> +
> +    native = auto()
> +    """Create and populate mempool using native DPDK memory."""
> +    xmem = auto()
> +    """Create and populate mempool using externally and anonymously allocated area."""
> +    xmemhuge = auto()
> +    """Create and populate mempool using externally and anonymously allocated hugepage area."""
> +
> +
> +@dataclass(kw_only=True)
> +class AnonMempoolAllocationMode(Params):
> +    """Create mempool using native DPDK memory, but populate using anonymous memory.
> +
> +    Attributes:
> +        no_iova_contig: Enables to create mempool which is not IOVA contiguous.
> +    """
> +
> +    _mp_alloc: Literal["anon"] = field(default="anon", init=False, metadata=Params.long("mp-alloc"))
> +    no_iova_contig: Switch = None
> +
> +
> +@dataclass(slots=True, kw_only=True)
> +class TestPmdParams(EalParams):
> +    """The testpmd shell parameters.
> +
> +    Attributes:
> +        interactive_mode: Runs testpmd in interactive mode.
> +        auto_start: Start forwarding on initialization.
> +        tx_first: Start forwarding, after sending a burst of packets first.
> +        stats_period: Display statistics every ``PERIOD`` seconds, if interactive mode is disabled.
> +                      The default value is 0, which means that the statistics will not be displayed.
> +
> +                      .. note:: This flag should be used only in non-interactive mode.
> +        display_xstats: Display comma-separated list of extended statistics every ``PERIOD`` seconds
> +                        as specified in ``--stats-period`` or when used with interactive commands
> +                        that show Rx/Tx statistics (i.e. ‘show port stats’).
> +        nb_cores: Set the number of forwarding cores, where 1 <= N <= “number of cores” or
> +                  ``RTE_MAX_LCORE`` from the configuration file.
> +        coremask: Set the bitmask of the cores running the packet forwarding test. The main
> +                  lcore is reserved for command line parsing only and cannot be masked on for packet
> +                  forwarding.
> +        nb_ports: Set the number of forwarding ports, where 1 <= N <= “number of ports” on the board
> +                  or ``RTE_MAX_ETHPORTS`` from the configuration file. The default value is the
> +                  number of ports on the board.
> +        port_topology: Set port topology, where mode is paired (the default), chained or loop.
> +        portmask: Set the bitmask of the ports used by the packet forwarding test.
> +        portlist: Set the forwarding ports based on the user input used by the packet forwarding
> +                  test. ‘-‘ denotes a range of ports to set including the two specified port IDs ‘,’
> +                  separates multiple port values. Possible examples like –portlist=0,1 or
> +                  –portlist=0-2 or –portlist=0,1-2 etc.
> +        numa: Enable/disable NUMA-aware allocation of RX/TX rings and of RX memory buffers (mbufs).
> +        socket_num: Set the socket from which all memory is allocated in NUMA mode, where
> +                    0 <= N < number of sockets on the board.
> +        port_numa_config: Specify the socket on which the memory pool to be used by the port will be
> +                          allocated.
> +        ring_numa_config: Specify the socket on which the TX/RX rings for the port will be
> +                          allocated. Where flag is 1 for RX, 2 for TX, and 3 for RX and TX.
> +        total_num_mbufs: Set the number of mbufs to be allocated in the mbuf pools, where N > 1024.
> +        mbuf_size: Set the data size of the mbufs used to N bytes, where N < 65536.
> +                   If multiple mbuf-size values are specified the extra memory pools will be created
> +                   for allocating mbufs to receive packets with buffer splitting features.
> +        mbcache: Set the cache of mbuf memory pools to N, where 0 <= N <= 512.
> +        max_pkt_len: Set the maximum packet size to N bytes, where N >= 64.
> +        eth_peers_configfile: Use a configuration file containing the Ethernet addresses of
> +                              the peer ports.
> +        eth_peer: Set the MAC address XX:XX:XX:XX:XX:XX of the peer port N,
> +                  where 0 <= N < RTE_MAX_ETHPORTS.
> +        tx_ip: Set the source and destination IP address used when doing transmit only test.
> +               The defaults address values are source 198.18.0.1 and destination 198.18.0.2.
> +               These are special purpose addresses reserved for benchmarking (RFC 5735).
> +        tx_udp: Set the source and destination UDP port number for transmit test only test.
> +                The default port is the port 9 which is defined for the discard protocol (RFC 863).
> +        enable_lro: Enable large receive offload.
> +        max_lro_pkt_size: Set the maximum LRO aggregated packet size to N bytes, where N >= 64.
> +        disable_crc_strip: Disable hardware CRC stripping.
> +        enable_scatter: Enable scatter (multi-segment) RX.
> +        enable_hw_vlan: Enable hardware VLAN.
> +        enable_hw_vlan_filter: Enable hardware VLAN filter.
> +        enable_hw_vlan_strip: Enable hardware VLAN strip.
> +        enable_hw_vlan_extend: Enable hardware VLAN extend.
> +        enable_hw_qinq_strip: Enable hardware QINQ strip.
> +        pkt_drop_enabled: Enable per-queue packet drop for packets with no descriptors.
> +        rss: Receive Side Scaling setting.
> +        forward_mode: Set the forwarding mode.
> +        hairpin_mode: Set the hairpin port configuration.
> +        hairpin_queues: Set the number of hairpin queues per port to N, where 1 <= N <= 65535.
> +        burst: Set the number of packets per burst to N, where 1 <= N <= 512.
> +        enable_rx_cksum: Enable hardware RX checksum offload.
> +        rx_queues: Set the number of RX queues per port to N, where 1 <= N <= 65535.
> +        rx_ring: Set the RX rings parameters.
> +        no_flush_rx: Don’t flush the RX streams before starting forwarding. Used mainly with
> +                     the PCAP PMD.
> +        rx_segments_offsets: Set the offsets of packet segments on receiving
> +                             if split feature is engaged.
> +        rx_segments_length: Set the length of segments to scatter packets on receiving
> +                            if split feature is engaged.
> +        multi_rx_mempool: Enable multiple mbuf pools per Rx queue.
> +        rx_shared_queue: Create queues in shared Rx queue mode if device supports. Shared Rx queues
> +                         are grouped per X ports. X defaults to UINT32_MAX, implies all ports join
> +                         share group 1. Forwarding engine “shared-rxq” should be used for shared Rx
> +                         queues. This engine does Rx only and update stream statistics accordingly.
> +        rx_offloads: Set the bitmask of RX queue offloads.
> +        rx_mq_mode: Set the RX multi queue mode which can be enabled.
> +        tx_queues: Set the number of TX queues per port to N, where 1 <= N <= 65535.
> +        tx_ring: Set the TX rings params.
> +        tx_offloads: Set the hexadecimal bitmask of TX queue offloads.
> +        eth_link_speed: Set a forced link speed to the ethernet port. E.g. 1000 for 1Gbps.
> +        disable_link_check: Disable check on link status when starting/stopping ports.
> +        disable_device_start: Do not automatically start all ports. This allows testing
> +                              configuration of rx and tx queues before device is started
> +                              for the first time.
> +        no_lsc_interrupt: Disable LSC interrupts for all ports, even those supporting it.
> +        no_rmv_interrupt: Disable RMV interrupts for all ports, even those supporting it.
> +        bitrate_stats: Set the logical core N to perform bitrate calculation.
> +        latencystats: Set the logical core N to perform latency and jitter calculations.
> +        print_events: Enable printing the occurrence of the designated events.
> +                      Using :attr:`TestPmdEvent.ALL` will enable all of them.
> +        mask_events: Disable printing the occurrence of the designated events.
> +                     Using :attr:`TestPmdEvent.ALL` will disable all of them.
> +        flow_isolate_all: Providing this parameter requests flow API isolated mode on all ports at
> +                          initialization time. It ensures all traffic is received through the
> +                          configured flow rules only (see flow command). Ports that do not support
> +                          this mode are automatically discarded.
> +        disable_flow_flush: Disable port flow flush when stopping port.
> +                            This allows testing keep flow rules or shared flow objects across
> +                            restart.
> +        hot_plug: Enable device event monitor mechanism for hotplug.
> +        vxlan_gpe_port: Set the UDP port number of tunnel VXLAN-GPE to N.
> +        geneve_parsed_port: Set the UDP port number that is used for parsing the GENEVE protocol
> +                            to N. HW may be configured with another tunnel Geneve port.
> +        lock_all_memory: Enable/disable locking all memory. Disabled by default.
> +        mempool_allocation_mode: Set mempool allocation mode.
> +        record_core_cycles: Enable measurement of CPU cycles per packet.
> +        record_burst_status: Enable display of RX and TX burst stats.
> +    """
> +
> +    interactive_mode: Switch = field(default=True, metadata=Params.short("i"))
> +    auto_start: Switch = field(default=None, metadata=Params.short("a"))
> +    tx_first: Switch = None
> +    stats_period: int | None = None
> +    display_xstats: list[str] | None = field(
> +        default=None, metadata=Params.convert_value(comma_separated)
> +    )
> +    nb_cores: int | None = None
> +    coremask: int | None = field(default=None, metadata=Params.convert_value(hex))
> +    nb_ports: int | None = None
> +    port_topology: PortTopology | None = PortTopology.paired
> +    portmask: int | None = field(default=None, metadata=Params.convert_value(hex))
> +    portlist: str | None = None  # TODO: can be ranges 0,1-3
> +
> +    numa: YesNoSwitch = True
> +    socket_num: int | None = None
> +    port_numa_config: list[PortNUMAConfig] | None = field(
> +        default=None, metadata=Params.convert_value(comma_separated)
> +    )
> +    ring_numa_config: list[RingNUMAConfig] | None = field(
> +        default=None, metadata=Params.convert_value(comma_separated)
> +    )
> +    total_num_mbufs: int | None = None
> +    mbuf_size: list[int] | None = field(
> +        default=None, metadata=Params.convert_value(comma_separated)
> +    )
> +    mbcache: int | None = None
> +    max_pkt_len: int | None = None
> +    eth_peers_configfile: PurePath | None = None
> +    eth_peer: list[EthPeer] | None = field(default=None, metadata=Params.multiple())
> +    tx_ip: TxIPAddrPair | None = TxIPAddrPair(source_ip="198.18.0.1", dest_ip="198.18.0.2")
> +    tx_udp: TxUDPPortPair | None = TxUDPPortPair(9)
> +    enable_lro: Switch = None
> +    max_lro_pkt_size: int | None = None
> +    disable_crc_strip: Switch = None
> +    enable_scatter: Switch = None
> +    enable_hw_vlan: Switch = None
> +    enable_hw_vlan_filter: Switch = None
> +    enable_hw_vlan_strip: Switch = None
> +    enable_hw_vlan_extend: Switch = None
> +    enable_hw_qinq_strip: Switch = None
> +    pkt_drop_enabled: Switch = field(default=None, metadata=Params.long("enable-drop-en"))
> +    rss: RSSSetting | None = None
> +    forward_mode: (
> +        SimpleForwardingModes
> +        | FlowGenForwardingMode
> +        | TXOnlyForwardingMode
> +        | NoisyForwardingMode
> +        | None
> +    ) = SimpleForwardingModes.io
> +    hairpin_mode: HairpinMode | None = HairpinMode(0)
> +    hairpin_queues: int | None = field(default=None, metadata=Params.long("hairpinq"))
> +    burst: int | None = None
> +    enable_rx_cksum: Switch = None
> +
> +    rx_queues: int | None = field(default=None, metadata=Params.long("rxq"))
> +    rx_ring: RXRingParams | None = None
> +    no_flush_rx: Switch = None
> +    rx_segments_offsets: list[int] | None = field(
> +        default=None, metadata=Params.long("rxoffs") | Params.convert_value(comma_separated)
> +    )
> +    rx_segments_length: list[int] | None = field(
> +        default=None, metadata=Params.long("rxpkts") | Params.convert_value(comma_separated)
> +    )
> +    multi_rx_mempool: Switch = None
> +    rx_shared_queue: Switch | int = field(default=None, metadata=Params.long("rxq-share"))
> +    rx_offloads: int | None = field(default=None, metadata=Params.convert_value(hex))
> +    rx_mq_mode: RXMultiQueueMode | None = (
> +        RXMultiQueueMode.DCB | RXMultiQueueMode.RSS | RXMultiQueueMode.VMDQ
> +    )
> +
> +    tx_queues: int | None = field(default=None, metadata=Params.long("txq"))
> +    tx_ring: TXRingParams | None = None
> +    tx_offloads: int | None = field(default=None, metadata=Params.convert_value(hex))
> +
> +    eth_link_speed: int | None = None
> +    disable_link_check: Switch = None
> +    disable_device_start: Switch = None
> +    no_lsc_interrupt: Switch = None
> +    no_rmv_interrupt: Switch = None
> +    bitrate_stats: int | None = None
> +    latencystats: int | None = None
> +    print_events: list[Event] | None = field(
> +        default=None, metadata=Params.multiple() | Params.long("print-event")
> +    )
> +    mask_events: list[Event] | None = field(
> +        default_factory=lambda: [Event.intr_lsc],
> +        metadata=Params.multiple() | Params.long("mask-event"),
> +    )
> +
> +    flow_isolate_all: Switch = None
> +    disable_flow_flush: Switch = None
> +
> +    hot_plug: Switch = None
> +    vxlan_gpe_port: int | None = None
> +    geneve_parsed_port: int | None = None
> +    lock_all_memory: YesNoSwitch = field(default=False, metadata=Params.long("mlockall"))
> +    mempool_allocation_mode: SimpleMempoolAllocationMode | AnonMempoolAllocationMode | None = field(
> +        default=None, metadata=Params.long("mp-alloc")
> +    )
> +    record_core_cycles: Switch = None
> +    record_burst_status: Switch = None
> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
> index 841d456a2f..ef3f23c582 100644
> --- a/dts/framework/remote_session/testpmd_shell.py
> +++ b/dts/framework/remote_session/testpmd_shell.py
> @@ -1,6 +1,7 @@
>  # SPDX-License-Identifier: BSD-3-Clause
>  # Copyright(c) 2023 University of New Hampshire
>  # Copyright(c) 2023 PANTHEON.tech s.r.o.
> +# Copyright(c) 2024 Arm Limited
>
>  """Testpmd interactive shell.
>
> @@ -16,14 +17,12 @@
>  """
>
>  import time
> -from enum import auto
>  from pathlib import PurePath
>  from typing import Callable, ClassVar
>
>  from framework.exception import InteractiveCommandExecutionError
> -from framework.params.eal import EalParams
> +from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
>  from framework.settings import SETTINGS
> -from framework.utils import StrEnum
>
>  from .interactive_shell import InteractiveShell
>
> @@ -50,37 +49,6 @@ def __str__(self) -> str:
>          return self.pci_address
>
>
> -class TestPmdForwardingModes(StrEnum):
> -    r"""The supported packet forwarding modes for :class:`~TestPmdShell`\s."""
> -
> -    #:
> -    io = auto()
> -    #:
> -    mac = auto()
> -    #:
> -    macswap = auto()
> -    #:
> -    flowgen = auto()
> -    #:
> -    rxonly = auto()
> -    #:
> -    txonly = auto()
> -    #:
> -    csum = auto()
> -    #:
> -    icmpecho = auto()
> -    #:
> -    ieee1588 = auto()
> -    #:
> -    noisy = auto()
> -    #:
> -    fivetswap = "5tswap"
> -    #:
> -    shared_rxq = "shared-rxq"
> -    #:
> -    recycle_mbufs = auto()
> -
> -
>  class TestPmdShell(InteractiveShell):
>      """Testpmd interactive shell.
>
> @@ -119,9 +87,7 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
>          Also find the number of pci addresses which were allowed on the command line when the app
>          was started.
>          """
> -        self._app_params += " -i --mask-event intr_lsc"
> -
> -        assert isinstance(self._app_params, EalParams)
> +        assert isinstance(self._app_params, TestPmdParams)
>
>          self.number_of_ports = (
>              len(self._app_params.ports) if self._app_params.ports is not None else 0
> @@ -213,7 +179,7 @@ def wait_link_status_up(self, port_id: int, timeout=SETTINGS.timeout) -> bool:
>              self._logger.error(f"The link for port {port_id} did not come up in the given timeout.")
>          return "Link status: up" in port_info
>
> -    def set_forward_mode(self, mode: TestPmdForwardingModes, verify: bool = True):
> +    def set_forward_mode(self, mode: SimpleForwardingModes, verify: bool = True):
>          """Set packet forwarding mode.
>
>          Args:
> diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
> index c6e93839cb..578b5a4318 100644
> --- a/dts/tests/TestSuite_pmd_buffer_scatter.py
> +++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
> @@ -23,7 +23,8 @@
>  from scapy.utils import hexstr  # type: ignore[import-untyped]
>
>  from framework.params import Params
> -from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
> +from framework.params.testpmd import SimpleForwardingModes
> +from framework.remote_session.testpmd_shell import TestPmdShell
>  from framework.test_suite import TestSuite
>
>
> @@ -113,7 +114,7 @@ def pmd_scatter(self, mbsize: int) -> None:
>              ),
>              privileged=True,
>          )
> -        testpmd.set_forward_mode(TestPmdForwardingModes.mac)
> +        testpmd.set_forward_mode(SimpleForwardingModes.mac)
>          testpmd.start()
>
>          for offset in [-1, 0, 1, 4, 5]:
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 2/8] dts: use Params for interactive shells
  2024-05-09 11:20   ` [PATCH v2 2/8] dts: use Params for interactive shells Luca Vizzarro
@ 2024-05-28 17:43     ` Nicholas Pratte
  2024-05-28 21:04     ` Jeremy Spewock
  2024-06-06 13:14     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-05-28 17:43 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Jeremy Spewock, Paul Szczepanek

Provided a review for the wrong version...

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Thu, May 9, 2024 at 7:21 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Make it so that interactive shells accept an implementation of `Params`
> for app arguments. Convert EalParameters to use `Params` instead.
>
> String command line parameters can still be supplied by using the
> `Params.from_str()` method.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>  .../remote_session/interactive_shell.py       |  12 +-
>  dts/framework/remote_session/testpmd_shell.py |  11 +-
>  dts/framework/testbed_model/node.py           |   6 +-
>  dts/framework/testbed_model/os_session.py     |   4 +-
>  dts/framework/testbed_model/sut_node.py       | 124 ++++++++----------
>  dts/tests/TestSuite_pmd_buffer_scatter.py     |   3 +-
>  6 files changed, 77 insertions(+), 83 deletions(-)
>
> diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
> index 074a541279..9da66d1c7e 100644
> --- a/dts/framework/remote_session/interactive_shell.py
> +++ b/dts/framework/remote_session/interactive_shell.py
> @@ -1,5 +1,6 @@
>  # SPDX-License-Identifier: BSD-3-Clause
>  # Copyright(c) 2023 University of New Hampshire
> +# Copyright(c) 2024 Arm Limited
>
>  """Common functionality for interactive shell handling.
>
> @@ -21,6 +22,7 @@
>  from paramiko import Channel, SSHClient, channel  # type: ignore[import-untyped]
>
>  from framework.logger import DTSLogger
> +from framework.params import Params
>  from framework.settings import SETTINGS
>
>
> @@ -40,7 +42,7 @@ class InteractiveShell(ABC):
>      _ssh_channel: Channel
>      _logger: DTSLogger
>      _timeout: float
> -    _app_args: str
> +    _app_params: Params
>
>      #: Prompt to expect at the end of output when sending a command.
>      #: This is often overridden by subclasses.
> @@ -63,7 +65,7 @@ def __init__(
>          interactive_session: SSHClient,
>          logger: DTSLogger,
>          get_privileged_command: Callable[[str], str] | None,
> -        app_args: str = "",
> +        app_params: Params = Params(),
>          timeout: float = SETTINGS.timeout,
>      ) -> None:
>          """Create an SSH channel during initialization.
> @@ -74,7 +76,7 @@ def __init__(
>              get_privileged_command: A method for modifying a command to allow it to use
>                  elevated privileges. If :data:`None`, the application will not be started
>                  with elevated privileges.
> -            app_args: The command line arguments to be passed to the application on startup.
> +            app_params: The command line parameters to be passed to the application on startup.
>              timeout: The timeout used for the SSH channel that is dedicated to this interactive
>                  shell. This timeout is for collecting output, so if reading from the buffer
>                  and no output is gathered within the timeout, an exception is thrown.
> @@ -87,7 +89,7 @@ def __init__(
>          self._ssh_channel.set_combine_stderr(True)  # combines stdout and stderr streams
>          self._logger = logger
>          self._timeout = timeout
> -        self._app_args = app_args
> +        self._app_params = app_params
>          self._start_application(get_privileged_command)
>
>      def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
> @@ -100,7 +102,7 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
>              get_privileged_command: A function (but could be any callable) that produces
>                  the version of the command with elevated privileges.
>          """
> -        start_command = f"{self.path} {self._app_args}"
> +        start_command = f"{self.path} {self._app_params}"
>          if get_privileged_command is not None:
>              start_command = get_privileged_command(start_command)
>          self.send_command(start_command)
> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
> index cb2ab6bd00..7eced27096 100644
> --- a/dts/framework/remote_session/testpmd_shell.py
> +++ b/dts/framework/remote_session/testpmd_shell.py
> @@ -22,6 +22,7 @@
>
>  from framework.exception import InteractiveCommandExecutionError
>  from framework.settings import SETTINGS
> +from framework.testbed_model.sut_node import EalParams
>  from framework.utils import StrEnum
>
>  from .interactive_shell import InteractiveShell
> @@ -118,8 +119,14 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
>          Also find the number of pci addresses which were allowed on the command line when the app
>          was started.
>          """
> -        self._app_args += " -i --mask-event intr_lsc"
> -        self.number_of_ports = self._app_args.count("-a ")
> +        self._app_params += " -i --mask-event intr_lsc"
> +
> +        assert isinstance(self._app_params, EalParams)
> +
> +        self.number_of_ports = (
> +            len(self._app_params.ports) if self._app_params.ports is not None else 0
> +        )
> +
>          super()._start_application(get_privileged_command)
>
>      def start(self, verify: bool = True) -> None:
> diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
> index 74061f6262..6af4f25a3c 100644
> --- a/dts/framework/testbed_model/node.py
> +++ b/dts/framework/testbed_model/node.py
> @@ -2,6 +2,7 @@
>  # Copyright(c) 2010-2014 Intel Corporation
>  # Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
>  # Copyright(c) 2022-2023 University of New Hampshire
> +# Copyright(c) 2024 Arm Limited
>
>  """Common functionality for node management.
>
> @@ -24,6 +25,7 @@
>  )
>  from framework.exception import ConfigurationError
>  from framework.logger import DTSLogger, get_dts_logger
> +from framework.params import Params
>  from framework.settings import SETTINGS
>
>  from .cpu import (
> @@ -199,7 +201,7 @@ def create_interactive_shell(
>          shell_cls: Type[InteractiveShellType],
>          timeout: float = SETTINGS.timeout,
>          privileged: bool = False,
> -        app_args: str = "",
> +        app_params: Params = Params(),
>      ) -> InteractiveShellType:
>          """Factory for interactive session handlers.
>
> @@ -222,7 +224,7 @@ def create_interactive_shell(
>              shell_cls,
>              timeout,
>              privileged,
> -            app_args,
> +            app_params,
>          )
>
>      def filter_lcores(
> diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
> index d5bf7e0401..1a77aee532 100644
> --- a/dts/framework/testbed_model/os_session.py
> +++ b/dts/framework/testbed_model/os_session.py
> @@ -1,6 +1,7 @@
>  # SPDX-License-Identifier: BSD-3-Clause
>  # Copyright(c) 2023 PANTHEON.tech s.r.o.
>  # Copyright(c) 2023 University of New Hampshire
> +# Copyright(c) 2024 Arm Limited
>
>  """OS-aware remote session.
>
> @@ -29,6 +30,7 @@
>
>  from framework.config import Architecture, NodeConfiguration, NodeInfo
>  from framework.logger import DTSLogger
> +from framework.params import Params
>  from framework.remote_session import (
>      CommandResult,
>      InteractiveRemoteSession,
> @@ -134,7 +136,7 @@ def create_interactive_shell(
>          shell_cls: Type[InteractiveShellType],
>          timeout: float,
>          privileged: bool,
> -        app_args: str,
> +        app_args: Params,
>      ) -> InteractiveShellType:
>          """Factory for interactive session handlers.
>
> diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
> index 97aa26d419..c886590979 100644
> --- a/dts/framework/testbed_model/sut_node.py
> +++ b/dts/framework/testbed_model/sut_node.py
> @@ -2,6 +2,7 @@
>  # Copyright(c) 2010-2014 Intel Corporation
>  # Copyright(c) 2023 PANTHEON.tech s.r.o.
>  # Copyright(c) 2023 University of New Hampshire
> +# Copyright(c) 2024 Arm Limited
>
>  """System under test (DPDK + hardware) node.
>
> @@ -14,8 +15,9 @@
>  import os
>  import tarfile
>  import time
> +from dataclasses import dataclass, field
>  from pathlib import PurePath
> -from typing import Type
> +from typing import Literal, Type
>
>  from framework.config import (
>      BuildTargetConfiguration,
> @@ -23,6 +25,7 @@
>      NodeInfo,
>      SutNodeConfiguration,
>  )
> +from framework.params import Params, Switch
>  from framework.remote_session import CommandResult
>  from framework.settings import SETTINGS
>  from framework.utils import MesonArgs
> @@ -34,62 +37,42 @@
>  from .virtual_device import VirtualDevice
>
>
> -class EalParameters(object):
> -    """The environment abstraction layer parameters.
> -
> -    The string representation can be created by converting the instance to a string.
> -    """
> +def _port_to_pci(port: Port) -> str:
> +    return port.pci
>
> -    def __init__(
> -        self,
> -        lcore_list: LogicalCoreList,
> -        memory_channels: int,
> -        prefix: str,
> -        no_pci: bool,
> -        vdevs: list[VirtualDevice],
> -        ports: list[Port],
> -        other_eal_param: str,
> -    ):
> -        """Initialize the parameters according to inputs.
> -
> -        Process the parameters into the format used on the command line.
>
> -        Args:
> -            lcore_list: The list of logical cores to use.
> -            memory_channels: The number of memory channels to use.
> -            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
> -            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
> -            vdevs: Virtual devices, e.g.::
> +@dataclass(kw_only=True)
> +class EalParams(Params):
> +    """The environment abstraction layer parameters.
>
> -                vdevs=[
> -                    VirtualDevice('net_ring0'),
> -                    VirtualDevice('net_ring1')
> -                ]
> -            ports: The list of ports to allow.
> -            other_eal_param: user defined DPDK EAL parameters, e.g.:
> +    Attributes:
> +        lcore_list: The list of logical cores to use.
> +        memory_channels: The number of memory channels to use.
> +        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
> +        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
> +        vdevs: Virtual devices, e.g.::
> +            vdevs=[
> +                VirtualDevice('net_ring0'),
> +                VirtualDevice('net_ring1')
> +            ]
> +        ports: The list of ports to allow.
> +        other_eal_param: user defined DPDK EAL parameters, e.g.:
>                  ``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._ports = " ".join(f"-a {port.pci}" for port in ports)
> -        self._other_eal_param = other_eal_param
> -
> -    def __str__(self) -> str:
> -        """Create the EAL string."""
> -        return (
> -            f"{self._lcore_list} "
> -            f"{self._memory_channels} "
> -            f"{self._prefix} "
> -            f"{self._no_pci} "
> -            f"{self._vdevs} "
> -            f"{self._ports} "
> -            f"{self._other_eal_param}"
> -        )
> +    """
> +
> +    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
> +    memory_channels: int = field(metadata=Params.short("n"))
> +    prefix: str = field(metadata=Params.long("file-prefix"))
> +    no_pci: Switch
> +    vdevs: list[VirtualDevice] | None = field(
> +        default=None, metadata=Params.multiple() | Params.long("vdev")
> +    )
> +    ports: list[Port] | None = field(
> +        default=None,
> +        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
> +    )
> +    other_eal_param: Params | None = None
> +    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
>
>
>  class SutNode(Node):
> @@ -350,11 +333,11 @@ def create_eal_parameters(
>          ascending_cores: bool = True,
>          prefix: str = "dpdk",
>          append_prefix_timestamp: bool = True,
> -        no_pci: bool = False,
> +        no_pci: Switch = None,
>          vdevs: list[VirtualDevice] | None = None,
>          ports: list[Port] | None = None,
>          other_eal_param: str = "",
> -    ) -> "EalParameters":
> +    ) -> EalParams:
>          """Compose the EAL parameters.
>
>          Process the list of cores and the DPDK prefix and pass that along with
> @@ -393,24 +376,21 @@ def create_eal_parameters(
>          if prefix:
>              self._dpdk_prefix_list.append(prefix)
>
> -        if vdevs is None:
> -            vdevs = []
> -
>          if ports is None:
>              ports = self.ports
>
> -        return EalParameters(
> +        return EalParams(
>              lcore_list=lcore_list,
>              memory_channels=self.config.memory_channels,
>              prefix=prefix,
>              no_pci=no_pci,
>              vdevs=vdevs,
>              ports=ports,
> -            other_eal_param=other_eal_param,
> +            other_eal_param=Params.from_str(other_eal_param),
>          )
>
>      def run_dpdk_app(
> -        self, app_path: PurePath, eal_args: "EalParameters", timeout: float = 30
> +        self, app_path: PurePath, eal_params: EalParams, timeout: float = 30
>      ) -> CommandResult:
>          """Run DPDK application on the remote node.
>
> @@ -419,14 +399,14 @@ def run_dpdk_app(
>
>          Args:
>              app_path: The remote path to the DPDK application.
> -            eal_args: EAL parameters to run the DPDK application with.
> +            eal_params: EAL parameters to run the DPDK application with.
>              timeout: Wait at most this long in seconds for `command` execution to complete.
>
>          Returns:
>              The result of the DPDK app execution.
>          """
>          return self.main_session.send_command(
> -            f"{app_path} {eal_args}", timeout, privileged=True, verify=True
> +            f"{app_path} {eal_params}", timeout, privileged=True, verify=True
>          )
>
>      def configure_ipv4_forwarding(self, enable: bool) -> None:
> @@ -442,8 +422,8 @@ def create_interactive_shell(
>          shell_cls: Type[InteractiveShellType],
>          timeout: float = SETTINGS.timeout,
>          privileged: bool = False,
> -        app_parameters: str = "",
> -        eal_parameters: EalParameters | None = None,
> +        app_params: Params = Params(),
> +        eal_params: EalParams | None = None,
>      ) -> InteractiveShellType:
>          """Extend the factory for interactive session handlers.
>
> @@ -459,26 +439,26 @@ def create_interactive_shell(
>                  reading from the buffer and don't receive any data within the timeout
>                  it will throw an error.
>              privileged: Whether to run the shell with administrative privileges.
> -            eal_parameters: List of EAL parameters to use to launch the app. If this
> +            app_params: The parameters to be passed to the application.
> +            eal_params: List of EAL parameters to use to launch the app. If this
>                  isn't provided or an empty string is passed, it will default to calling
>                  :meth:`create_eal_parameters`.
> -            app_parameters: Additional arguments to pass into the application on the
> -                command-line.
>
>          Returns:
>              An instance of the desired interactive application shell.
>          """
>          # We need to append the build directory and add EAL parameters for DPDK apps
>          if shell_cls.dpdk_app:
> -            if not eal_parameters:
> -                eal_parameters = self.create_eal_parameters()
> -            app_parameters = f"{eal_parameters} -- {app_parameters}"
> +            if eal_params is None:
> +                eal_params = self.create_eal_parameters()
> +            eal_params.append_str(str(app_params))
> +            app_params = eal_params
>
>              shell_cls.path = self.main_session.join_remote_path(
>                  self.remote_dpdk_build_dir, shell_cls.path
>              )
>
> -        return super().create_interactive_shell(shell_cls, timeout, privileged, app_parameters)
> +        return super().create_interactive_shell(shell_cls, timeout, privileged, app_params)
>
>      def bind_ports_to_driver(self, for_dpdk: bool = True) -> None:
>          """Bind all ports on the SUT to a driver.
> diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
> index a020682e8d..c6e93839cb 100644
> --- a/dts/tests/TestSuite_pmd_buffer_scatter.py
> +++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
> @@ -22,6 +22,7 @@
>  from scapy.packet import Raw  # type: ignore[import-untyped]
>  from scapy.utils import hexstr  # type: ignore[import-untyped]
>
> +from framework.params import Params
>  from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
>  from framework.test_suite import TestSuite
>
> @@ -103,7 +104,7 @@ def pmd_scatter(self, mbsize: int) -> None:
>          """
>          testpmd = self.sut_node.create_interactive_shell(
>              TestPmdShell,
> -            app_parameters=(
> +            app_params=Params.from_str(
>                  "--mbcache=200 "
>                  f"--mbuf-size={mbsize} "
>                  "--max-pkt-len=9000 "
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 2/8] dts: use Params for interactive shells
  2024-05-09 11:20   ` [PATCH v2 2/8] dts: use Params for interactive shells Luca Vizzarro
  2024-05-28 17:43     ` Nicholas Pratte
@ 2024-05-28 21:04     ` Jeremy Spewock
  2024-06-06 13:14     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Jeremy Spewock @ 2024-05-28 21:04 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Paul Szczepanek

Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 3/8] dts: refactor EalParams
  2024-05-09 11:20   ` [PATCH v2 3/8] dts: refactor EalParams Luca Vizzarro
  2024-05-28 15:44     ` Nicholas Pratte
@ 2024-05-28 21:05     ` Jeremy Spewock
  2024-06-06 13:17     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Jeremy Spewock @ 2024-05-28 21:05 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Paul Szczepanek

Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 5/8] dts: add testpmd shell params
  2024-05-09 11:20   ` [PATCH v2 5/8] dts: add testpmd shell params Luca Vizzarro
  2024-05-28 15:53     ` Nicholas Pratte
@ 2024-05-28 21:05     ` Jeremy Spewock
  2024-05-29 15:59       ` Luca Vizzarro
  1 sibling, 1 reply; 159+ messages in thread
From: Jeremy Spewock @ 2024-05-28 21:05 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Paul Szczepanek

This looks good, the only comment I had was in some classes the
docstrings didn't get updated to what was discussed previously in the
comments (making sure the comments are included in the class'
docstring). I tried to point out a few places where I noticed it.
Other than those comments however:

Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>

On Thu, May 9, 2024 at 7:21 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Implement all the testpmd shell parameters into a data structure.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>  dts/framework/params/testpmd.py               | 608 ++++++++++++++++++
>  dts/framework/remote_session/testpmd_shell.py |  42 +-
>  dts/tests/TestSuite_pmd_buffer_scatter.py     |   5 +-
>  3 files changed, 615 insertions(+), 40 deletions(-)
>  create mode 100644 dts/framework/params/testpmd.py
>
<snip>
> +
> +
> +class PortTopology(StrEnum):
> +    """Enum representing the port topology."""
> +
> +    paired = auto()
> +    """In paired mode, the forwarding is between pairs of ports, e.g.: (0,1), (2,3), (4,5)."""
> +    chained = auto()
> +    """In chained mode, the forwarding is to the next available port in the port mask, e.g.:
> +    (0,1), (1,2), (2,0).
> +
> +    The ordering of the ports can be changed using the portlist testpmd runtime function.
> +    """
> +    loop = auto()
> +    """In loop mode, ingress traffic is simply transmitted back on the same interface."""
> +

This should likely be the comment style for class vars: `#:`

> +
<snip>
> +
> +@convert_str(hex_from_flag_value)
> +@unique
> +class HairpinMode(Flag):
> +    """Flag representing the hairpin mode."""
> +
> +    TWO_PORTS_LOOP = 1 << 0
> +    """Two hairpin ports loop."""
> +    TWO_PORTS_PAIRED = 1 << 1
> +    """Two hairpin ports paired."""
> +    EXPLICIT_TX_FLOW = 1 << 4
> +    """Explicit Tx flow rule."""
> +    FORCE_RX_QUEUE_MEM_SETTINGS = 1 << 8
> +    """Force memory settings of hairpin RX queue."""
> +    FORCE_TX_QUEUE_MEM_SETTINGS = 1 << 9
> +    """Force memory settings of hairpin TX queue."""
> +    RX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 12
> +    """Hairpin RX queues will use locked device memory."""
> +    RX_QUEUE_USE_RTE_MEMORY = 1 << 13
> +    """Hairpin RX queues will use RTE memory."""
> +    TX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 16
> +    """Hairpin TX queues will use locked device memory."""
> +    TX_QUEUE_USE_RTE_MEMORY = 1 << 18
> +    """Hairpin TX queues will use RTE memory."""
> +

Same thing in this class, these should likely be documented as class
vars with `#:`

> +
<snip>
> +class SimpleMempoolAllocationMode(StrEnum):
> +    """Enum representing simple mempool allocation modes."""
> +
> +    native = auto()
> +    """Create and populate mempool using native DPDK memory."""
> +    xmem = auto()
> +    """Create and populate mempool using externally and anonymously allocated area."""
> +    xmemhuge = auto()
> +    """Create and populate mempool using externally and anonymously allocated hugepage area."""
> +

Also here. Same as the previous, should likely be `#:`

> +
> +@dataclass(kw_only=True)
<snip>
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 6/8] dts: use testpmd params for scatter test suite
  2024-05-28 15:49     ` Nicholas Pratte
@ 2024-05-28 21:06       ` Jeremy Spewock
  0 siblings, 0 replies; 159+ messages in thread
From: Jeremy Spewock @ 2024-05-28 21:06 UTC (permalink / raw)
  To: Nicholas Pratte; +Cc: Luca Vizzarro, dev, Juraj Linkeš, Paul Szczepanek

Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 7/8] dts: rework interactive shells
  2024-05-09 11:20   ` [PATCH v2 7/8] dts: rework interactive shells Luca Vizzarro
  2024-05-28 15:50     ` Nicholas Pratte
@ 2024-05-28 21:07     ` Jeremy Spewock
  2024-05-29 15:57       ` Luca Vizzarro
  1 sibling, 1 reply; 159+ messages in thread
From: Jeremy Spewock @ 2024-05-28 21:07 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Paul Szczepanek

On Thu, May 9, 2024 at 7:21 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> The way nodes and interactive shells interact makes it difficult to
> develop for static type checking and hinting. The current system relies
> on a top-down approach, attempting to give a generic interface to the
> test developer, hiding the interaction of concrete shell classes as much
> as possible. When working with strong typing this approach is not ideal,
> as Python's implementation of generics is still rudimentary.
>
> This rework reverses the tests interaction to a bottom-up approach,
> allowing the test developer to call concrete shell classes directly,
> and let them ingest nodes independently. While also re-enforcing type
> checking and making the code easier to read.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>  dts/framework/params/eal.py                   |   6 +-
>  dts/framework/remote_session/dpdk_shell.py    | 104 ++++++++++++++++
>  .../remote_session/interactive_shell.py       |  75 +++++++-----
>  dts/framework/remote_session/python_shell.py  |   4 +-
>  dts/framework/remote_session/testpmd_shell.py |  64 +++++-----
>  dts/framework/testbed_model/node.py           |  36 +-----
>  dts/framework/testbed_model/os_session.py     |  36 +-----
>  dts/framework/testbed_model/sut_node.py       | 112 +-----------------
>  .../testbed_model/traffic_generator/scapy.py  |   4 +-
>  dts/tests/TestSuite_hello_world.py            |   7 +-
>  dts/tests/TestSuite_pmd_buffer_scatter.py     |  21 ++--
>  dts/tests/TestSuite_smoke_tests.py            |   2 +-
>  12 files changed, 201 insertions(+), 270 deletions(-)
>  create mode 100644 dts/framework/remote_session/dpdk_shell.py
>
<snip>
>      def __init__(
>          self,
> -        interactive_session: SSHClient,
> -        logger: DTSLogger,
> -        get_privileged_command: Callable[[str], str] | None,
> +        node: Node,
>          app_params: Params = Params(),
> +        privileged: bool = False,
>          timeout: float = SETTINGS.timeout,
> +        start_on_init: bool = True,
>      ) -> None:
>          """Create an SSH channel during initialization.
>
>          Args:
> -            interactive_session: The SSH session dedicated to interactive shells.
> -            logger: The logger instance this session will use.
> -            get_privileged_command: A method for modifying a command to allow it to use
> -                elevated privileges. If :data:`None`, the application will not be started
> -                with elevated privileges.
> +            node: The node on which to run start the interactive shell.
>              app_params: The command line parameters to be passed to the application on startup.
> +            privileged: Enables the shell to run as superuser.
>              timeout: The timeout used for the SSH channel that is dedicated to this interactive
>                  shell. This timeout is for collecting output, so if reading from the buffer
>                  and no output is gathered within the timeout, an exception is thrown.
> +            start_on_init: Start interactive shell automatically after object initialisation.
>          """
> -        self._interactive_session = interactive_session
> -        self._ssh_channel = self._interactive_session.invoke_shell()
> +        self._node = node
> +        self._logger = node._logger
> +        self._app_params = app_params
> +        self._privileged = privileged
> +        self._timeout = timeout
> +        # Ensure path is properly formatted for the host
> +        self._update_path(self._node.main_session.join_remote_path(self.path))
> +
> +        self.__post_init__()
> +
> +        if start_on_init:
> +            self.start_application()

What's the reason for including start_on_init? Is there a time when
someone would create an application but not want to start it when they
create it? It seems like it is always true currently and I'm not sure
we would want it to be delayed otherwise (except in cases like the
context manager patch where we want to enforce that it is only started
for specific periods of time).

> +
> +    def __post_init__(self):

Is the name of this method meant to mimic that of the dataclasses? It
might also make sense to call it something like `_post_init()` as just
a regular private method, I'm not sure it matters either way.
Additionally, I think in other super classes which contained functions
that were optionally implemented by subclasses we omitted the `pass`
and just left the function stub empty other than the doc-string.
Either way this does the same thing, but it might be better to make
them consistent one way or the other.

> +        """Overridable. Method called after the object init and before application start."""
> +        pass
> +
<snip>
>
> -    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
> +    def start_application(self) -> None:
>          """Starts a new interactive application based on the path to the app.
>
>          This method is often overridden by subclasses as their process for
>          starting may look different.
> -
> -        Args:
> -            get_privileged_command: A function (but could be any callable) that produces
> -                the version of the command with elevated privileges.
>          """
> -        start_command = f"{self.path} {self._app_params}"
> -        if get_privileged_command is not None:
> -            start_command = get_privileged_command(start_command)
> +        self._setup_ssh_channel()
> +
> +        start_command = self._make_start_command()
> +        if self._privileged:
> +            start_command = self._node.main_session._get_privileged_command(start_command)
>          self.send_command(start_command)
>
> +    def _make_start_command(self) -> str:

It might make sense to put this above the start_application method
since that is where it gets called.

> +        """Makes the command that starts the interactive shell."""
> +        return f"{self.path} {self._app_params or ''}"
> +
<snip>
> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
> index ef3f23c582..92930d7fbb 100644
> --- a/dts/framework/remote_session/testpmd_shell.py
> +++ b/dts/framework/remote_session/testpmd_shell.py
> @@ -7,9 +7,7 @@
>
>  Typical usage example in a TestSuite::
>
> -    testpmd_shell = self.sut_node.create_interactive_shell(
> -            TestPmdShell, privileged=True
> -        )
> +    testpmd_shell = TestPmdShell()

Maybe adding a parameter to this instantiation in the example would
still be useful. So something like `TestPmdShell(self.sut_node)`
instead just because this cannot be instantiated without any
arguments.

>      devices = testpmd_shell.get_devices()
>      for device in devices:
>          print(device)
<snip>
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 1/8] dts: add params manipulation module
  2024-05-09 11:20   ` [PATCH v2 1/8] dts: add params manipulation module Luca Vizzarro
  2024-05-28 15:40     ` Nicholas Pratte
@ 2024-05-28 21:08     ` Jeremy Spewock
  2024-06-06  9:19     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Jeremy Spewock @ 2024-05-28 21:08 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Paul Szczepanek

I think there was just one typo, otherwise:

Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>

On Thu, May 9, 2024 at 7:21 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> This commit introduces a new "params" module, which adds a new way
> to manage command line parameters. The provided Params dataclass
> is able to read the fields of its child class and produce a string
> representation to supply to the command line. Any data structure
> that is intended to represent command line parameters can inherit it.
>
> The main purpose is to make it easier to represent data structures that
> map to parameters. Aiding quicker development, while minimising code
> bloat.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
<snip>
> +def comma_separated(values: Iterable[Any]) -> str:
> +    """Converts an iterable in a comma-separated string."""

I think this was meant to be "...an iterable into a comma-separated..."

> +    return ",".join([str(value).strip() for value in values if value is not None])
> +
> +
<snip>
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 4/8] dts: remove module-wide imports
  2024-05-09 11:20   ` [PATCH v2 4/8] dts: remove module-wide imports Luca Vizzarro
  2024-05-28 15:45     ` Nicholas Pratte
@ 2024-05-28 21:08     ` Jeremy Spewock
  2024-06-06 13:21     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Jeremy Spewock @ 2024-05-28 21:08 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Paul Szczepanek

Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 8/8] dts: use Unpack for type checking and hinting
  2024-05-09 11:20   ` [PATCH v2 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
  2024-05-28 15:50     ` Nicholas Pratte
@ 2024-05-28 21:08     ` Jeremy Spewock
  1 sibling, 0 replies; 159+ messages in thread
From: Jeremy Spewock @ 2024-05-28 21:08 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Paul Szczepanek

Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 7/8] dts: rework interactive shells
  2024-05-28 21:07     ` Jeremy Spewock
@ 2024-05-29 15:57       ` Luca Vizzarro
  0 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-05-29 15:57 UTC (permalink / raw)
  To: Jeremy Spewock; +Cc: dev, Juraj Linkeš, Paul Szczepanek

On 28/05/2024 22:07, Jeremy Spewock wrote:
>> +        if start_on_init:
>> +            self.start_application()
> 
> What's the reason for including start_on_init? Is there a time when
> someone would create an application but not want to start it when they
> create it? It seems like it is always true currently and I'm not sure
> we would want it to be delayed otherwise (except in cases like the
> context manager patch where we want to enforce that it is only started
> for specific periods of time).

You are right in thinking that currently it's not used. I left it there 
on purpose as I see the potential case in which we want to manipulate 
the settings before starting the shell. Albeit, as you said it's not 
needed currently. I can omit it if we don't care for now.

>> +    def __post_init__(self):
> 
> Is the name of this method meant to mimic that of the dataclasses? It
> might also make sense to call it something like `_post_init()` as just
> a regular private method, I'm not sure it matters either way.
Ack.
> Additionally, I think in other super classes which contained functions
> that were optionally implemented by subclasses we omitted the `pass`
> and just left the function stub empty other than the doc-string.
> Either way this does the same thing, but it might be better to make
> them consistent one way or the other.

Ack.

>> -    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
>> +    def start_application(self) -> None:
>> <snip>
>> +    def _make_start_command(self) -> str:
> 
> It might make sense to put this above the start_application method
> since that is where it gets called.

Ack.

>> -    testpmd_shell = self.sut_node.create_interactive_shell(
>> -            TestPmdShell, privileged=True
>> -        )
>> +    testpmd_shell = TestPmdShell()
> 
> Maybe adding a parameter to this instantiation in the example would
> still be useful. So something like `TestPmdShell(self.sut_node)`
> instead just because this cannot be instantiated without any
> arguments.

Yes! Well spotted, this was missed.


^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 5/8] dts: add testpmd shell params
  2024-05-28 21:05     ` Jeremy Spewock
@ 2024-05-29 15:59       ` Luca Vizzarro
  2024-05-29 17:11         ` Jeremy Spewock
  0 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-05-29 15:59 UTC (permalink / raw)
  To: Jeremy Spewock; +Cc: dev, Juraj Linkeš, Paul Szczepanek

On 28/05/2024 22:05, Jeremy Spewock wrote:
> This looks good, the only comment I had was in some classes the
> docstrings didn't get updated to what was discussed previously in the
> comments (making sure the comments are included in the class'
> docstring). I tried to point out a few places where I noticed it.

Apologies for asking again, as I may have totally missed them. Would you 
be able to clarify or give examples to what you are referring to exactly?

Will update the docstring format for the Enum class members.


^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 5/8] dts: add testpmd shell params
  2024-05-29 15:59       ` Luca Vizzarro
@ 2024-05-29 17:11         ` Jeremy Spewock
  0 siblings, 0 replies; 159+ messages in thread
From: Jeremy Spewock @ 2024-05-29 17:11 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Paul Szczepanek

On Wed, May 29, 2024 at 11:59 AM Luca Vizzarro <Luca.Vizzarro@arm.com> wrote:
>
> On 28/05/2024 22:05, Jeremy Spewock wrote:
> > This looks good, the only comment I had was in some classes the
> > docstrings didn't get updated to what was discussed previously in the
> > comments (making sure the comments are included in the class'
> > docstring). I tried to point out a few places where I noticed it.
>
> Apologies for asking again, as I may have totally missed them. Would you
> be able to clarify or give examples to what you are referring to exactly?
>
> Will update the docstring format for the Enum class members.
>
Apologies for the confusion, all I was referring to were the Enum
class members and the HairpinMode class.

^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v3 0/8] dts: add testpmd params
  2024-03-26 19:04 [PATCH 0/6] dts: add testpmd params and statefulness Luca Vizzarro
                   ` (6 preceding siblings ...)
  2024-05-09 11:20 ` [PATCH v2 0/8] dts: add testpmd params Luca Vizzarro
@ 2024-05-30 15:24 ` Luca Vizzarro
  2024-05-30 15:24   ` [PATCH v3 1/8] dts: add params manipulation module Luca Vizzarro
                     ` (7 more replies)
  2024-06-17 14:42 ` [PATCH v4 0/8] dts: add testpmd params and statefulness Luca Vizzarro
                   ` (3 subsequent siblings)
  11 siblings, 8 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-05-30 15:24 UTC (permalink / raw)
  To: dev; +Cc: Juraj Linkeš, Jeremy Spewock, Luca Vizzarro

v3:
- refactored InteractiveShell methods
- fixed docstrings
v2:
- refactored the params module
- strengthened typing of the params module
- moved the params module into its own package
- refactored EalParams and TestPmdParams and
  moved under the params package
- reworked interactions between nodes and shells
- refactored imports leading to circular dependencies

---
Depends-on: series-32026 ("dts: update mypy and clean up")
---

Luca Vizzarro (8):
  dts: add params manipulation module
  dts: use Params for interactive shells
  dts: refactor EalParams
  dts: remove module-wide imports
  dts: add testpmd shell params
  dts: use testpmd params for scatter test suite
  dts: rework interactive shells
  dts: use Unpack for type checking and hinting

 dts/framework/params/__init__.py              | 274 ++++++++
 dts/framework/params/eal.py                   |  50 ++
 dts/framework/params/testpmd.py               | 609 ++++++++++++++++++
 dts/framework/params/types.py                 | 133 ++++
 dts/framework/remote_session/__init__.py      |   5 +-
 dts/framework/remote_session/dpdk_shell.py    | 104 +++
 .../remote_session/interactive_shell.py       |  82 ++-
 dts/framework/remote_session/python_shell.py  |   4 +-
 dts/framework/remote_session/testpmd_shell.py | 102 ++-
 dts/framework/runner.py                       |   4 +-
 dts/framework/test_suite.py                   |   5 +-
 dts/framework/testbed_model/__init__.py       |   7 -
 dts/framework/testbed_model/node.py           |  36 +-
 dts/framework/testbed_model/os_session.py     |  38 +-
 dts/framework/testbed_model/sut_node.py       | 182 +-----
 .../testbed_model/traffic_generator/scapy.py  |   6 +-
 dts/tests/TestSuite_hello_world.py            |   9 +-
 dts/tests/TestSuite_pmd_buffer_scatter.py     |  21 +-
 dts/tests/TestSuite_smoke_tests.py            |   4 +-
 19 files changed, 1296 insertions(+), 379 deletions(-)
 create mode 100644 dts/framework/params/__init__.py
 create mode 100644 dts/framework/params/eal.py
 create mode 100644 dts/framework/params/testpmd.py
 create mode 100644 dts/framework/params/types.py
 create mode 100644 dts/framework/remote_session/dpdk_shell.py

-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v3 1/8] dts: add params manipulation module
  2024-05-30 15:24 ` [PATCH v3 " Luca Vizzarro
@ 2024-05-30 15:24   ` Luca Vizzarro
  2024-05-30 20:12     ` Jeremy Spewock
  2024-05-31 15:19     ` Nicholas Pratte
  2024-05-30 15:24   ` [PATCH v3 2/8] dts: use Params for interactive shells Luca Vizzarro
                     ` (6 subsequent siblings)
  7 siblings, 2 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-05-30 15:24 UTC (permalink / raw)
  To: dev; +Cc: Juraj Linkeš, Jeremy Spewock, Luca Vizzarro, Paul Szczepanek

This commit introduces a new "params" module, which adds a new way
to manage command line parameters. The provided Params dataclass
is able to read the fields of its child class and produce a string
representation to supply to the command line. Any data structure
that is intended to represent command line parameters can inherit it.

The main purpose is to make it easier to represent data structures that
map to parameters. Aiding quicker development, while minimising code
bloat.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 dts/framework/params/__init__.py | 274 +++++++++++++++++++++++++++++++
 1 file changed, 274 insertions(+)
 create mode 100644 dts/framework/params/__init__.py

diff --git a/dts/framework/params/__init__.py b/dts/framework/params/__init__.py
new file mode 100644
index 0000000000..18fedcf1ff
--- /dev/null
+++ b/dts/framework/params/__init__.py
@@ -0,0 +1,274 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Parameter manipulation module.
+
+This module provides :class:`Params` which can be used to model any data structure
+that is meant to represent any command parameters.
+"""
+
+from dataclasses import dataclass, fields
+from enum import Flag
+from typing import Any, Callable, Iterable, Literal, Reversible, TypedDict, cast
+
+from typing_extensions import Self
+
+#: Type for a function taking one argument.
+FnPtr = Callable[[Any], Any]
+#: Type for a switch parameter.
+Switch = Literal[True, None]
+#: Type for a yes/no switch parameter.
+YesNoSwitch = Literal[True, False, None]
+
+
+def _reduce_functions(funcs: Reversible[FnPtr]) -> FnPtr:
+    """Reduces an iterable of :attr:`FnPtr` from end to start to a composite function.
+
+    If the iterable is empty, the created function just returns its fed value back.
+    """
+
+    def composite_function(value: Any):
+        for fn in reversed(funcs):
+            value = fn(value)
+        return value
+
+    return composite_function
+
+
+def convert_str(*funcs: FnPtr):
+    """Decorator that makes the ``__str__`` method a composite function created from its arguments.
+
+    The :attr:`FnPtr`s fed to the decorator are executed from right to left
+    in the arguments list order.
+
+    Example:
+    .. code:: python
+
+        @convert_str(hex_from_flag_value)
+        class BitMask(enum.Flag):
+            A = auto()
+            B = auto()
+
+    will allow ``BitMask`` to render as a hexadecimal value.
+    """
+
+    def _class_decorator(original_class):
+        original_class.__str__ = _reduce_functions(funcs)
+        return original_class
+
+    return _class_decorator
+
+
+def comma_separated(values: Iterable[Any]) -> str:
+    """Converts an iterable into a comma-separated string."""
+    return ",".join([str(value).strip() for value in values if value is not None])
+
+
+def bracketed(value: str) -> str:
+    """Adds round brackets to the input."""
+    return f"({value})"
+
+
+def str_from_flag_value(flag: Flag) -> str:
+    """Returns the value from a :class:`enum.Flag` as a string."""
+    return str(flag.value)
+
+
+def hex_from_flag_value(flag: Flag) -> str:
+    """Returns the value from a :class:`enum.Flag` converted to hexadecimal."""
+    return hex(flag.value)
+
+
+class ParamsModifier(TypedDict, total=False):
+    """Params modifiers dict compatible with the :func:`dataclasses.field` metadata parameter."""
+
+    #:
+    Params_value_only: bool
+    #:
+    Params_short: str
+    #:
+    Params_long: str
+    #:
+    Params_multiple: bool
+    #:
+    Params_convert_value: Reversible[FnPtr]
+
+
+@dataclass
+class Params:
+    """Dataclass that renders its fields into command line arguments.
+
+    The parameter name is taken from the field name by default. The following:
+
+    .. code:: python
+
+        name: str | None = "value"
+
+    is rendered as ``--name=value``.
+    Through :func:`dataclasses.field` the resulting parameter can be manipulated by applying
+    this class' metadata modifier functions.
+
+    To use fields as switches, set the value to ``True`` to render them. If you
+    use a yes/no switch you can also set ``False`` which would render a switch
+    prefixed with ``--no-``. Examples:
+
+    .. code:: python
+
+        interactive: Switch = True  # renders --interactive
+        numa: YesNoSwitch   = False # renders --no-numa
+
+    Setting ``None`` will prevent it from being rendered. The :attr:`~Switch` type alias is provided
+    for regular switches, whereas :attr:`~YesNoSwitch` is offered for yes/no ones.
+
+    An instance of a dataclass inheriting ``Params`` can also be assigned to an attribute,
+    this helps with grouping parameters together.
+    The attribute holding the dataclass will be ignored and the latter will just be rendered as
+    expected.
+    """
+
+    _suffix = ""
+    """Holder of the plain text value of Params when called directly. A suffix for child classes."""
+
+    """========= BEGIN FIELD METADATA MODIFIER FUNCTIONS ========"""
+
+    @staticmethod
+    def value_only() -> ParamsModifier:
+        """Injects the value of the attribute as-is without flag.
+
+        Metadata modifier for :func:`dataclasses.field`.
+        """
+        return ParamsModifier(Params_value_only=True)
+
+    @staticmethod
+    def short(name: str) -> ParamsModifier:
+        """Overrides any parameter name with the given short option.
+
+        Metadata modifier for :func:`dataclasses.field`.
+
+        Example:
+        .. code:: python
+
+            logical_cores: str | None = field(default="1-4", metadata=Params.short("l"))
+
+        will render as ``-l=1-4`` instead of ``--logical-cores=1-4``.
+        """
+        return ParamsModifier(Params_short=name)
+
+    @staticmethod
+    def long(name: str) -> ParamsModifier:
+        """Overrides the inferred parameter name to the specified one.
+
+        Metadata modifier for :func:`dataclasses.field`.
+
+        Example:
+        .. code:: python
+
+            x_name: str | None = field(default="y", metadata=Params.long("x"))
+
+        will render as ``--x=y``, but the field is accessed and modified through ``x_name``.
+        """
+        return ParamsModifier(Params_long=name)
+
+    @staticmethod
+    def multiple() -> ParamsModifier:
+        """Specifies that this parameter is set multiple times. Must be a list.
+
+        Metadata modifier for :func:`dataclasses.field`.
+
+        Example:
+        .. code:: python
+
+            ports: list[int] | None = field(
+                default_factory=lambda: [0, 1, 2],
+                metadata=Params.multiple() | Params.long("port")
+            )
+
+        will render as ``--port=0 --port=1 --port=2``. Note that modifiers can be chained like
+        in this example.
+        """
+        return ParamsModifier(Params_multiple=True)
+
+    @classmethod
+    def convert_value(cls, *funcs: FnPtr) -> ParamsModifier:
+        """Takes in a variable number of functions to convert the value text representation.
+
+        Metadata modifier for :func:`dataclasses.field`.
+
+        The ``metadata`` keyword argument can be used to chain metadata modifiers together.
+
+        Functions can be chained together, executed from right to left in the arguments list order.
+
+        Example:
+        .. code:: python
+
+            hex_bitmask: int | None = field(
+                default=0b1101,
+                metadata=Params.convert_value(hex) | Params.long("mask")
+            )
+
+        will render as ``--mask=0xd``.
+        """
+        return ParamsModifier(Params_convert_value=funcs)
+
+    """========= END FIELD METADATA MODIFIER FUNCTIONS ========"""
+
+    def append_str(self, text: str) -> None:
+        """Appends a string at the end of the string representation."""
+        self._suffix += text
+
+    def __iadd__(self, text: str) -> Self:
+        """Appends a string at the end of the string representation."""
+        self.append_str(text)
+        return self
+
+    @classmethod
+    def from_str(cls, text: str) -> Self:
+        """Creates a plain Params object from a string."""
+        obj = cls()
+        obj.append_str(text)
+        return obj
+
+    @staticmethod
+    def _make_switch(
+        name: str, is_short: bool = False, is_no: bool = False, value: str | None = None
+    ) -> str:
+        prefix = f"{'-' if is_short else '--'}{'no-' if is_no else ''}"
+        name = name.replace("_", "-")
+        value = f"{' ' if is_short else '='}{value}" if value else ""
+        return f"{prefix}{name}{value}"
+
+    def __str__(self) -> str:
+        """Returns a string of command-line-ready arguments from the class fields."""
+        arguments: list[str] = []
+
+        for field in fields(self):
+            value = getattr(self, field.name)
+            modifiers = cast(ParamsModifier, field.metadata)
+
+            if value is None:
+                continue
+
+            value_only = modifiers.get("Params_value_only", False)
+            if isinstance(value, Params) or value_only:
+                arguments.append(str(value))
+                continue
+
+            # take the short modifier, or the long modifier, or infer from field name
+            switch_name = modifiers.get("Params_short", modifiers.get("Params_long", field.name))
+            is_short = "Params_short" in modifiers
+
+            if isinstance(value, bool):
+                arguments.append(self._make_switch(switch_name, is_short, is_no=(not value)))
+                continue
+
+            convert = _reduce_functions(modifiers.get("Params_convert_value", []))
+            multiple = modifiers.get("Params_multiple", False)
+
+            values = value if multiple else [value]
+            for value in values:
+                arguments.append(self._make_switch(switch_name, is_short, value=convert(value)))
+
+        if self._suffix:
+            arguments.append(self._suffix)
+
+        return " ".join(arguments)
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v3 2/8] dts: use Params for interactive shells
  2024-05-30 15:24 ` [PATCH v3 " Luca Vizzarro
  2024-05-30 15:24   ` [PATCH v3 1/8] dts: add params manipulation module Luca Vizzarro
@ 2024-05-30 15:24   ` Luca Vizzarro
  2024-05-30 20:12     ` Jeremy Spewock
  2024-05-31 15:20     ` Nicholas Pratte
  2024-05-30 15:25   ` [PATCH v3 3/8] dts: refactor EalParams Luca Vizzarro
                     ` (5 subsequent siblings)
  7 siblings, 2 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-05-30 15:24 UTC (permalink / raw)
  To: dev; +Cc: Juraj Linkeš, Jeremy Spewock, Luca Vizzarro, Paul Szczepanek

Make it so that interactive shells accept an implementation of `Params`
for app arguments. Convert EalParameters to use `Params` instead.

String command line parameters can still be supplied by using the
`Params.from_str()` method.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 .../remote_session/interactive_shell.py       |  12 +-
 dts/framework/remote_session/testpmd_shell.py |  11 +-
 dts/framework/testbed_model/node.py           |   6 +-
 dts/framework/testbed_model/os_session.py     |   4 +-
 dts/framework/testbed_model/sut_node.py       | 124 ++++++++----------
 dts/tests/TestSuite_pmd_buffer_scatter.py     |   3 +-
 6 files changed, 77 insertions(+), 83 deletions(-)

diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
index 074a541279..9da66d1c7e 100644
--- a/dts/framework/remote_session/interactive_shell.py
+++ b/dts/framework/remote_session/interactive_shell.py
@@ -1,5 +1,6 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright(c) 2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """Common functionality for interactive shell handling.
 
@@ -21,6 +22,7 @@
 from paramiko import Channel, SSHClient, channel  # type: ignore[import-untyped]
 
 from framework.logger import DTSLogger
+from framework.params import Params
 from framework.settings import SETTINGS
 
 
@@ -40,7 +42,7 @@ class InteractiveShell(ABC):
     _ssh_channel: Channel
     _logger: DTSLogger
     _timeout: float
-    _app_args: str
+    _app_params: Params
 
     #: Prompt to expect at the end of output when sending a command.
     #: This is often overridden by subclasses.
@@ -63,7 +65,7 @@ def __init__(
         interactive_session: SSHClient,
         logger: DTSLogger,
         get_privileged_command: Callable[[str], str] | None,
-        app_args: str = "",
+        app_params: Params = Params(),
         timeout: float = SETTINGS.timeout,
     ) -> None:
         """Create an SSH channel during initialization.
@@ -74,7 +76,7 @@ def __init__(
             get_privileged_command: A method for modifying a command to allow it to use
                 elevated privileges. If :data:`None`, the application will not be started
                 with elevated privileges.
-            app_args: The command line arguments to be passed to the application on startup.
+            app_params: The command line parameters to be passed to the application on startup.
             timeout: The timeout used for the SSH channel that is dedicated to this interactive
                 shell. This timeout is for collecting output, so if reading from the buffer
                 and no output is gathered within the timeout, an exception is thrown.
@@ -87,7 +89,7 @@ def __init__(
         self._ssh_channel.set_combine_stderr(True)  # combines stdout and stderr streams
         self._logger = logger
         self._timeout = timeout
-        self._app_args = app_args
+        self._app_params = app_params
         self._start_application(get_privileged_command)
 
     def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
@@ -100,7 +102,7 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
             get_privileged_command: A function (but could be any callable) that produces
                 the version of the command with elevated privileges.
         """
-        start_command = f"{self.path} {self._app_args}"
+        start_command = f"{self.path} {self._app_params}"
         if get_privileged_command is not None:
             start_command = get_privileged_command(start_command)
         self.send_command(start_command)
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index cb2ab6bd00..7eced27096 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -22,6 +22,7 @@
 
 from framework.exception import InteractiveCommandExecutionError
 from framework.settings import SETTINGS
+from framework.testbed_model.sut_node import EalParams
 from framework.utils import StrEnum
 
 from .interactive_shell import InteractiveShell
@@ -118,8 +119,14 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
         Also find the number of pci addresses which were allowed on the command line when the app
         was started.
         """
-        self._app_args += " -i --mask-event intr_lsc"
-        self.number_of_ports = self._app_args.count("-a ")
+        self._app_params += " -i --mask-event intr_lsc"
+
+        assert isinstance(self._app_params, EalParams)
+
+        self.number_of_ports = (
+            len(self._app_params.ports) if self._app_params.ports is not None else 0
+        )
+
         super()._start_application(get_privileged_command)
 
     def start(self, verify: bool = True) -> None:
diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
index 74061f6262..6af4f25a3c 100644
--- a/dts/framework/testbed_model/node.py
+++ b/dts/framework/testbed_model/node.py
@@ -2,6 +2,7 @@
 # Copyright(c) 2010-2014 Intel Corporation
 # Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2022-2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """Common functionality for node management.
 
@@ -24,6 +25,7 @@
 )
 from framework.exception import ConfigurationError
 from framework.logger import DTSLogger, get_dts_logger
+from framework.params import Params
 from framework.settings import SETTINGS
 
 from .cpu import (
@@ -199,7 +201,7 @@ def create_interactive_shell(
         shell_cls: Type[InteractiveShellType],
         timeout: float = SETTINGS.timeout,
         privileged: bool = False,
-        app_args: str = "",
+        app_params: Params = Params(),
     ) -> InteractiveShellType:
         """Factory for interactive session handlers.
 
@@ -222,7 +224,7 @@ def create_interactive_shell(
             shell_cls,
             timeout,
             privileged,
-            app_args,
+            app_params,
         )
 
     def filter_lcores(
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
index d5bf7e0401..1a77aee532 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright(c) 2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """OS-aware remote session.
 
@@ -29,6 +30,7 @@
 
 from framework.config import Architecture, NodeConfiguration, NodeInfo
 from framework.logger import DTSLogger
+from framework.params import Params
 from framework.remote_session import (
     CommandResult,
     InteractiveRemoteSession,
@@ -134,7 +136,7 @@ def create_interactive_shell(
         shell_cls: Type[InteractiveShellType],
         timeout: float,
         privileged: bool,
-        app_args: str,
+        app_args: Params,
     ) -> InteractiveShellType:
         """Factory for interactive session handlers.
 
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index 97aa26d419..c886590979 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -2,6 +2,7 @@
 # Copyright(c) 2010-2014 Intel Corporation
 # Copyright(c) 2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """System under test (DPDK + hardware) node.
 
@@ -14,8 +15,9 @@
 import os
 import tarfile
 import time
+from dataclasses import dataclass, field
 from pathlib import PurePath
-from typing import Type
+from typing import Literal, Type
 
 from framework.config import (
     BuildTargetConfiguration,
@@ -23,6 +25,7 @@
     NodeInfo,
     SutNodeConfiguration,
 )
+from framework.params import Params, Switch
 from framework.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
@@ -34,62 +37,42 @@
 from .virtual_device import VirtualDevice
 
 
-class EalParameters(object):
-    """The environment abstraction layer parameters.
-
-    The string representation can be created by converting the instance to a string.
-    """
+def _port_to_pci(port: Port) -> str:
+    return port.pci
 
-    def __init__(
-        self,
-        lcore_list: LogicalCoreList,
-        memory_channels: int,
-        prefix: str,
-        no_pci: bool,
-        vdevs: list[VirtualDevice],
-        ports: list[Port],
-        other_eal_param: str,
-    ):
-        """Initialize the parameters according to inputs.
-
-        Process the parameters into the format used on the command line.
 
-        Args:
-            lcore_list: The list of logical cores to use.
-            memory_channels: The number of memory channels to use.
-            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
-            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
-            vdevs: Virtual devices, e.g.::
+@dataclass(kw_only=True)
+class EalParams(Params):
+    """The environment abstraction layer parameters.
 
-                vdevs=[
-                    VirtualDevice('net_ring0'),
-                    VirtualDevice('net_ring1')
-                ]
-            ports: The list of ports to allow.
-            other_eal_param: user defined DPDK EAL parameters, e.g.:
+    Attributes:
+        lcore_list: The list of logical cores to use.
+        memory_channels: The number of memory channels to use.
+        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
+        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
+        vdevs: Virtual devices, e.g.::
+            vdevs=[
+                VirtualDevice('net_ring0'),
+                VirtualDevice('net_ring1')
+            ]
+        ports: The list of ports to allow.
+        other_eal_param: user defined DPDK EAL parameters, e.g.:
                 ``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._ports = " ".join(f"-a {port.pci}" for port in ports)
-        self._other_eal_param = other_eal_param
-
-    def __str__(self) -> str:
-        """Create the EAL string."""
-        return (
-            f"{self._lcore_list} "
-            f"{self._memory_channels} "
-            f"{self._prefix} "
-            f"{self._no_pci} "
-            f"{self._vdevs} "
-            f"{self._ports} "
-            f"{self._other_eal_param}"
-        )
+    """
+
+    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
+    memory_channels: int = field(metadata=Params.short("n"))
+    prefix: str = field(metadata=Params.long("file-prefix"))
+    no_pci: Switch
+    vdevs: list[VirtualDevice] | None = field(
+        default=None, metadata=Params.multiple() | Params.long("vdev")
+    )
+    ports: list[Port] | None = field(
+        default=None,
+        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
+    )
+    other_eal_param: Params | None = None
+    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
 
 
 class SutNode(Node):
@@ -350,11 +333,11 @@ def create_eal_parameters(
         ascending_cores: bool = True,
         prefix: str = "dpdk",
         append_prefix_timestamp: bool = True,
-        no_pci: bool = False,
+        no_pci: Switch = None,
         vdevs: list[VirtualDevice] | None = None,
         ports: list[Port] | None = None,
         other_eal_param: str = "",
-    ) -> "EalParameters":
+    ) -> EalParams:
         """Compose the EAL parameters.
 
         Process the list of cores and the DPDK prefix and pass that along with
@@ -393,24 +376,21 @@ def create_eal_parameters(
         if prefix:
             self._dpdk_prefix_list.append(prefix)
 
-        if vdevs is None:
-            vdevs = []
-
         if ports is None:
             ports = self.ports
 
-        return EalParameters(
+        return EalParams(
             lcore_list=lcore_list,
             memory_channels=self.config.memory_channels,
             prefix=prefix,
             no_pci=no_pci,
             vdevs=vdevs,
             ports=ports,
-            other_eal_param=other_eal_param,
+            other_eal_param=Params.from_str(other_eal_param),
         )
 
     def run_dpdk_app(
-        self, app_path: PurePath, eal_args: "EalParameters", timeout: float = 30
+        self, app_path: PurePath, eal_params: EalParams, timeout: float = 30
     ) -> CommandResult:
         """Run DPDK application on the remote node.
 
@@ -419,14 +399,14 @@ def run_dpdk_app(
 
         Args:
             app_path: The remote path to the DPDK application.
-            eal_args: EAL parameters to run the DPDK application with.
+            eal_params: EAL parameters to run the DPDK application with.
             timeout: Wait at most this long in seconds for `command` execution to complete.
 
         Returns:
             The result of the DPDK app execution.
         """
         return self.main_session.send_command(
-            f"{app_path} {eal_args}", timeout, privileged=True, verify=True
+            f"{app_path} {eal_params}", timeout, privileged=True, verify=True
         )
 
     def configure_ipv4_forwarding(self, enable: bool) -> None:
@@ -442,8 +422,8 @@ def create_interactive_shell(
         shell_cls: Type[InteractiveShellType],
         timeout: float = SETTINGS.timeout,
         privileged: bool = False,
-        app_parameters: str = "",
-        eal_parameters: EalParameters | None = None,
+        app_params: Params = Params(),
+        eal_params: EalParams | None = None,
     ) -> InteractiveShellType:
         """Extend the factory for interactive session handlers.
 
@@ -459,26 +439,26 @@ def create_interactive_shell(
                 reading from the buffer and don't receive any data within the timeout
                 it will throw an error.
             privileged: Whether to run the shell with administrative privileges.
-            eal_parameters: List of EAL parameters to use to launch the app. If this
+            app_params: The parameters to be passed to the application.
+            eal_params: List of EAL parameters to use to launch the app. If this
                 isn't provided or an empty string is passed, it will default to calling
                 :meth:`create_eal_parameters`.
-            app_parameters: Additional arguments to pass into the application on the
-                command-line.
 
         Returns:
             An instance of the desired interactive application shell.
         """
         # We need to append the build directory and add EAL parameters for DPDK apps
         if shell_cls.dpdk_app:
-            if not eal_parameters:
-                eal_parameters = self.create_eal_parameters()
-            app_parameters = f"{eal_parameters} -- {app_parameters}"
+            if eal_params is None:
+                eal_params = self.create_eal_parameters()
+            eal_params.append_str(str(app_params))
+            app_params = eal_params
 
             shell_cls.path = self.main_session.join_remote_path(
                 self.remote_dpdk_build_dir, shell_cls.path
             )
 
-        return super().create_interactive_shell(shell_cls, timeout, privileged, app_parameters)
+        return super().create_interactive_shell(shell_cls, timeout, privileged, app_params)
 
     def bind_ports_to_driver(self, for_dpdk: bool = True) -> None:
         """Bind all ports on the SUT to a driver.
diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index a020682e8d..c6e93839cb 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -22,6 +22,7 @@
 from scapy.packet import Raw  # type: ignore[import-untyped]
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
+from framework.params import Params
 from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
 from framework.test_suite import TestSuite
 
@@ -103,7 +104,7 @@ def pmd_scatter(self, mbsize: int) -> None:
         """
         testpmd = self.sut_node.create_interactive_shell(
             TestPmdShell,
-            app_parameters=(
+            app_params=Params.from_str(
                 "--mbcache=200 "
                 f"--mbuf-size={mbsize} "
                 "--max-pkt-len=9000 "
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v3 3/8] dts: refactor EalParams
  2024-05-30 15:24 ` [PATCH v3 " Luca Vizzarro
  2024-05-30 15:24   ` [PATCH v3 1/8] dts: add params manipulation module Luca Vizzarro
  2024-05-30 15:24   ` [PATCH v3 2/8] dts: use Params for interactive shells Luca Vizzarro
@ 2024-05-30 15:25   ` Luca Vizzarro
  2024-05-30 20:12     ` Jeremy Spewock
  2024-05-31 15:21     ` Nicholas Pratte
  2024-05-30 15:25   ` [PATCH v3 4/8] dts: remove module-wide imports Luca Vizzarro
                     ` (4 subsequent siblings)
  7 siblings, 2 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-05-30 15:25 UTC (permalink / raw)
  To: dev; +Cc: Juraj Linkeš, Jeremy Spewock, Luca Vizzarro, Paul Szczepanek

Move EalParams to its own module to avoid circular dependencies.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 dts/framework/params/eal.py                   | 50 +++++++++++++++++++
 dts/framework/remote_session/testpmd_shell.py |  2 +-
 dts/framework/testbed_model/sut_node.py       | 42 +---------------
 3 files changed, 53 insertions(+), 41 deletions(-)
 create mode 100644 dts/framework/params/eal.py

diff --git a/dts/framework/params/eal.py b/dts/framework/params/eal.py
new file mode 100644
index 0000000000..bbdbc8f334
--- /dev/null
+++ b/dts/framework/params/eal.py
@@ -0,0 +1,50 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Module representing the DPDK EAL-related parameters."""
+
+from dataclasses import dataclass, field
+from typing import Literal
+
+from framework.params import Params, Switch
+from framework.testbed_model.cpu import LogicalCoreList
+from framework.testbed_model.port import Port
+from framework.testbed_model.virtual_device import VirtualDevice
+
+
+def _port_to_pci(port: Port) -> str:
+    return port.pci
+
+
+@dataclass(kw_only=True)
+class EalParams(Params):
+    """The environment abstraction layer parameters.
+
+    Attributes:
+        lcore_list: The list of logical cores to use.
+        memory_channels: The number of memory channels to use.
+        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
+        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
+        vdevs: Virtual devices, e.g.::
+            vdevs=[
+                VirtualDevice('net_ring0'),
+                VirtualDevice('net_ring1')
+            ]
+        ports: The list of ports to allow.
+        other_eal_param: user defined DPDK EAL parameters, e.g.:
+                ``other_eal_param='--single-file-segments'``
+    """
+
+    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
+    memory_channels: int = field(metadata=Params.short("n"))
+    prefix: str = field(metadata=Params.long("file-prefix"))
+    no_pci: Switch = None
+    vdevs: list[VirtualDevice] | None = field(
+        default=None, metadata=Params.multiple() | Params.long("vdev")
+    )
+    ports: list[Port] | None = field(
+        default=None,
+        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
+    )
+    other_eal_param: Params | None = None
+    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index 7eced27096..841d456a2f 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -21,8 +21,8 @@
 from typing import Callable, ClassVar
 
 from framework.exception import InteractiveCommandExecutionError
+from framework.params.eal import EalParams
 from framework.settings import SETTINGS
-from framework.testbed_model.sut_node import EalParams
 from framework.utils import StrEnum
 
 from .interactive_shell import InteractiveShell
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index c886590979..e1163106a3 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -15,9 +15,8 @@
 import os
 import tarfile
 import time
-from dataclasses import dataclass, field
 from pathlib import PurePath
-from typing import Literal, Type
+from typing import Type
 
 from framework.config import (
     BuildTargetConfiguration,
@@ -26,6 +25,7 @@
     SutNodeConfiguration,
 )
 from framework.params import Params, Switch
+from framework.params.eal import EalParams
 from framework.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
@@ -37,44 +37,6 @@
 from .virtual_device import VirtualDevice
 
 
-def _port_to_pci(port: Port) -> str:
-    return port.pci
-
-
-@dataclass(kw_only=True)
-class EalParams(Params):
-    """The environment abstraction layer parameters.
-
-    Attributes:
-        lcore_list: The list of logical cores to use.
-        memory_channels: The number of memory channels to use.
-        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
-        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
-        vdevs: Virtual devices, e.g.::
-            vdevs=[
-                VirtualDevice('net_ring0'),
-                VirtualDevice('net_ring1')
-            ]
-        ports: The list of ports to allow.
-        other_eal_param: user defined DPDK EAL parameters, e.g.:
-                ``other_eal_param='--single-file-segments'``
-    """
-
-    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
-    memory_channels: int = field(metadata=Params.short("n"))
-    prefix: str = field(metadata=Params.long("file-prefix"))
-    no_pci: Switch
-    vdevs: list[VirtualDevice] | None = field(
-        default=None, metadata=Params.multiple() | Params.long("vdev")
-    )
-    ports: list[Port] | None = field(
-        default=None,
-        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
-    )
-    other_eal_param: Params | None = None
-    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
-
-
 class SutNode(Node):
     """The system under test node.
 
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v3 4/8] dts: remove module-wide imports
  2024-05-30 15:24 ` [PATCH v3 " Luca Vizzarro
                     ` (2 preceding siblings ...)
  2024-05-30 15:25   ` [PATCH v3 3/8] dts: refactor EalParams Luca Vizzarro
@ 2024-05-30 15:25   ` Luca Vizzarro
  2024-05-30 20:12     ` Jeremy Spewock
  2024-05-31 15:21     ` Nicholas Pratte
  2024-05-30 15:25   ` [PATCH v3 5/8] dts: add testpmd shell params Luca Vizzarro
                     ` (3 subsequent siblings)
  7 siblings, 2 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-05-30 15:25 UTC (permalink / raw)
  To: dev; +Cc: Juraj Linkeš, Jeremy Spewock, Luca Vizzarro, Paul Szczepanek

Remove the imports in the testbed_model and remote_session modules init
file, to avoid the initialisation of unneeded modules, thus removing or
limiting the risk of circular dependencies.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 dts/framework/remote_session/__init__.py               | 5 +----
 dts/framework/runner.py                                | 4 +++-
 dts/framework/test_suite.py                            | 5 ++++-
 dts/framework/testbed_model/__init__.py                | 7 -------
 dts/framework/testbed_model/os_session.py              | 4 ++--
 dts/framework/testbed_model/sut_node.py                | 2 +-
 dts/framework/testbed_model/traffic_generator/scapy.py | 2 +-
 dts/tests/TestSuite_hello_world.py                     | 2 +-
 dts/tests/TestSuite_smoke_tests.py                     | 2 +-
 9 files changed, 14 insertions(+), 19 deletions(-)

diff --git a/dts/framework/remote_session/__init__.py b/dts/framework/remote_session/__init__.py
index 1910c81c3c..29000a4642 100644
--- a/dts/framework/remote_session/__init__.py
+++ b/dts/framework/remote_session/__init__.py
@@ -18,11 +18,8 @@
 from framework.logger import DTSLogger
 
 from .interactive_remote_session import InteractiveRemoteSession
-from .interactive_shell import InteractiveShell
-from .python_shell import PythonShell
-from .remote_session import CommandResult, RemoteSession
+from .remote_session import RemoteSession
 from .ssh_session import SSHSession
-from .testpmd_shell import TestPmdShell
 
 
 def create_remote_session(
diff --git a/dts/framework/runner.py b/dts/framework/runner.py
index dfdee14802..687bc04f79 100644
--- a/dts/framework/runner.py
+++ b/dts/framework/runner.py
@@ -26,6 +26,9 @@
 from types import FunctionType
 from typing import Iterable, Sequence
 
+from framework.testbed_model.sut_node import SutNode
+from framework.testbed_model.tg_node import TGNode
+
 from .config import (
     BuildTargetConfiguration,
     Configuration,
@@ -51,7 +54,6 @@
     TestSuiteWithCases,
 )
 from .test_suite import TestSuite
-from .testbed_model import SutNode, TGNode
 
 
 class DTSRunner:
diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py
index 8768f756a6..9d3debb00f 100644
--- a/dts/framework/test_suite.py
+++ b/dts/framework/test_suite.py
@@ -20,9 +20,12 @@
 from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
 from scapy.packet import Packet, Padding  # type: ignore[import-untyped]
 
+from framework.testbed_model.port import Port, PortLink
+from framework.testbed_model.sut_node import SutNode
+from framework.testbed_model.tg_node import TGNode
+
 from .exception import TestCaseVerifyError
 from .logger import DTSLogger, get_dts_logger
-from .testbed_model import Port, PortLink, SutNode, TGNode
 from .testbed_model.traffic_generator import PacketFilteringConfig
 from .utils import get_packet_summaries
 
diff --git a/dts/framework/testbed_model/__init__.py b/dts/framework/testbed_model/__init__.py
index 6086512ca2..4f8a58c039 100644
--- a/dts/framework/testbed_model/__init__.py
+++ b/dts/framework/testbed_model/__init__.py
@@ -19,10 +19,3 @@
 """
 
 # pylama:ignore=W0611
-
-from .cpu import LogicalCoreCount, LogicalCoreCountFilter, LogicalCoreList
-from .node import Node
-from .port import Port, PortLink
-from .sut_node import SutNode
-from .tg_node import TGNode
-from .virtual_device import VirtualDevice
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
index 1a77aee532..e5f5fcbe0e 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -32,13 +32,13 @@
 from framework.logger import DTSLogger
 from framework.params import Params
 from framework.remote_session import (
-    CommandResult,
     InteractiveRemoteSession,
-    InteractiveShell,
     RemoteSession,
     create_interactive_session,
     create_remote_session,
 )
+from framework.remote_session.interactive_shell import InteractiveShell
+from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
 
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index e1163106a3..83ad06ae2d 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -26,7 +26,7 @@
 )
 from framework.params import Params, Switch
 from framework.params.eal import EalParams
-from framework.remote_session import CommandResult
+from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
 
diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py
index ed5467d825..7bc1c2cc08 100644
--- a/dts/framework/testbed_model/traffic_generator/scapy.py
+++ b/dts/framework/testbed_model/traffic_generator/scapy.py
@@ -25,7 +25,7 @@
 from scapy.packet import Packet  # type: ignore[import-untyped]
 
 from framework.config import OS, ScapyTrafficGeneratorConfig
-from framework.remote_session import PythonShell
+from framework.remote_session.python_shell import PythonShell
 from framework.settings import SETTINGS
 from framework.testbed_model.node import Node
 from framework.testbed_model.port import Port
diff --git a/dts/tests/TestSuite_hello_world.py b/dts/tests/TestSuite_hello_world.py
index fd7ff1534d..0d6995f260 100644
--- a/dts/tests/TestSuite_hello_world.py
+++ b/dts/tests/TestSuite_hello_world.py
@@ -8,7 +8,7 @@
 """
 
 from framework.test_suite import TestSuite
-from framework.testbed_model import (
+from framework.testbed_model.cpu import (
     LogicalCoreCount,
     LogicalCoreCountFilter,
     LogicalCoreList,
diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py
index a553e89662..ca678f662d 100644
--- a/dts/tests/TestSuite_smoke_tests.py
+++ b/dts/tests/TestSuite_smoke_tests.py
@@ -15,7 +15,7 @@
 import re
 
 from framework.config import PortConfig
-from framework.remote_session import TestPmdShell
+from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.settings import SETTINGS
 from framework.test_suite import TestSuite
 from framework.utils import REGEX_FOR_PCI_ADDRESS
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v3 5/8] dts: add testpmd shell params
  2024-05-30 15:24 ` [PATCH v3 " Luca Vizzarro
                     ` (3 preceding siblings ...)
  2024-05-30 15:25   ` [PATCH v3 4/8] dts: remove module-wide imports Luca Vizzarro
@ 2024-05-30 15:25   ` Luca Vizzarro
  2024-05-30 20:12     ` Jeremy Spewock
                       ` (2 more replies)
  2024-05-30 15:25   ` [PATCH v3 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
                     ` (2 subsequent siblings)
  7 siblings, 3 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-05-30 15:25 UTC (permalink / raw)
  To: dev; +Cc: Juraj Linkeš, Jeremy Spewock, Luca Vizzarro, Paul Szczepanek

Implement all the testpmd shell parameters into a data structure.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 dts/framework/params/testpmd.py               | 609 ++++++++++++++++++
 dts/framework/remote_session/testpmd_shell.py |  42 +-
 dts/tests/TestSuite_pmd_buffer_scatter.py     |   5 +-
 3 files changed, 616 insertions(+), 40 deletions(-)
 create mode 100644 dts/framework/params/testpmd.py

diff --git a/dts/framework/params/testpmd.py b/dts/framework/params/testpmd.py
new file mode 100644
index 0000000000..88d208d683
--- /dev/null
+++ b/dts/framework/params/testpmd.py
@@ -0,0 +1,609 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Module containing all the TestPmd-related parameter classes."""
+
+from dataclasses import dataclass, field
+from enum import EnumMeta, Flag, auto, unique
+from pathlib import PurePath
+from typing import Literal, NamedTuple
+
+from framework.params import (
+    Params,
+    Switch,
+    YesNoSwitch,
+    bracketed,
+    comma_separated,
+    convert_str,
+    hex_from_flag_value,
+    str_from_flag_value,
+)
+from framework.params.eal import EalParams
+from framework.utils import StrEnum
+
+
+class PortTopology(StrEnum):
+    """Enum representing the port topology."""
+
+    #: In paired mode, the forwarding is between pairs of ports, e.g.: (0,1), (2,3), (4,5).
+    paired = auto()
+
+    #: In chained mode, the forwarding is to the next available port in the port mask, e.g.:
+    #: (0,1), (1,2), (2,0).
+    #:
+    #: The ordering of the ports can be changed using the portlist testpmd runtime function.
+    chained = auto()
+
+    #: In loop mode, ingress traffic is simply transmitted back on the same interface.
+    loop = auto()
+
+
+@convert_str(bracketed, comma_separated)
+class PortNUMAConfig(NamedTuple):
+    """DPDK port to NUMA socket association tuple."""
+
+    #:
+    port: int
+    #:
+    socket: int
+
+
+@convert_str(str_from_flag_value)
+@unique
+class FlowDirection(Flag):
+    """Flag indicating the direction of the flow.
+
+    A bi-directional flow can be specified with the pipe:
+
+    >>> TestPmdFlowDirection.RX | TestPmdFlowDirection.TX
+    <TestPmdFlowDirection.TX|RX: 3>
+    """
+
+    #:
+    RX = 1 << 0
+    #:
+    TX = 1 << 1
+
+
+@convert_str(bracketed, comma_separated)
+class RingNUMAConfig(NamedTuple):
+    """Tuple associating DPDK port, direction of the flow and NUMA socket."""
+
+    #:
+    port: int
+    #:
+    direction: FlowDirection
+    #:
+    socket: int
+
+
+@convert_str(comma_separated)
+class EthPeer(NamedTuple):
+    """Tuple associating a MAC address to the specified DPDK port."""
+
+    #:
+    port_no: int
+    #:
+    mac_address: str
+
+
+@convert_str(comma_separated)
+class TxIPAddrPair(NamedTuple):
+    """Tuple specifying the source and destination IPs for the packets."""
+
+    #:
+    source_ip: str
+    #:
+    dest_ip: str
+
+
+@convert_str(comma_separated)
+class TxUDPPortPair(NamedTuple):
+    """Tuple specifying the UDP source and destination ports for the packets.
+
+    If leaving ``dest_port`` unspecified, ``source_port`` will be used for
+    the destination port as well.
+    """
+
+    #:
+    source_port: int
+    #:
+    dest_port: int | None = None
+
+
+@dataclass
+class DisableRSS(Params):
+    """Disables RSS (Receive Side Scaling)."""
+
+    _disable_rss: Literal[True] = field(
+        default=True, init=False, metadata=Params.long("disable-rss")
+    )
+
+
+@dataclass
+class SetRSSIPOnly(Params):
+    """Sets RSS (Receive Side Scaling) functions for IPv4/IPv6 only."""
+
+    _rss_ip: Literal[True] = field(default=True, init=False, metadata=Params.long("rss-ip"))
+
+
+@dataclass
+class SetRSSUDP(Params):
+    """Sets RSS (Receive Side Scaling) functions for IPv4/IPv6 and UDP."""
+
+    _rss_udp: Literal[True] = field(default=True, init=False, metadata=Params.long("rss-udp"))
+
+
+class RSSSetting(EnumMeta):
+    """Enum representing a RSS setting. Each property is a class that needs to be initialised."""
+
+    #:
+    Disabled = DisableRSS
+    #:
+    SetIPOnly = SetRSSIPOnly
+    #:
+    SetUDP = SetRSSUDP
+
+
+class SimpleForwardingModes(StrEnum):
+    r"""The supported packet forwarding modes for :class:`~TestPmdShell`\s."""
+
+    #:
+    io = auto()
+    #:
+    mac = auto()
+    #:
+    macswap = auto()
+    #:
+    rxonly = auto()
+    #:
+    csum = auto()
+    #:
+    icmpecho = auto()
+    #:
+    ieee1588 = auto()
+    #:
+    fivetswap = "5tswap"
+    #:
+    shared_rxq = "shared-rxq"
+    #:
+    recycle_mbufs = auto()
+
+
+@dataclass(kw_only=True)
+class TXOnlyForwardingMode(Params):
+    """Sets a TX-Only forwarding mode.
+
+    Attributes:
+        multi_flow: Generates multiple flows if set to True.
+        segments_length: Sets TX segment sizes or total packet length.
+    """
+
+    _forward_mode: Literal["txonly"] = field(
+        default="txonly", init=False, metadata=Params.long("forward-mode")
+    )
+    multi_flow: Switch = field(default=None, metadata=Params.long("txonly-multi-flow"))
+    segments_length: list[int] | None = field(
+        default=None, metadata=Params.long("txpkts") | Params.convert_value(comma_separated)
+    )
+
+
+@dataclass(kw_only=True)
+class FlowGenForwardingMode(Params):
+    """Sets a flowgen forwarding mode.
+
+    Attributes:
+        clones: Set the number of each packet clones to be sent. Sending clones reduces host CPU
+                load on creating packets and may help in testing extreme speeds or maxing out
+                Tx packet performance. N should be not zero, but less than ‘burst’ parameter.
+        flows: Set the number of flows to be generated, where 1 <= N <= INT32_MAX.
+        segments_length: Set TX segment sizes or total packet length.
+    """
+
+    _forward_mode: Literal["flowgen"] = field(
+        default="flowgen", init=False, metadata=Params.long("forward-mode")
+    )
+    clones: int | None = field(default=None, metadata=Params.long("flowgen-clones"))
+    flows: int | None = field(default=None, metadata=Params.long("flowgen-flows"))
+    segments_length: list[int] | None = field(
+        default=None, metadata=Params.long("txpkts") | Params.convert_value(comma_separated)
+    )
+
+
+@dataclass(kw_only=True)
+class NoisyForwardingMode(Params):
+    """Sets a noisy forwarding mode.
+
+    Attributes:
+        forward_mode: Set the noisy VNF forwarding mode.
+        tx_sw_buffer_size: Set the maximum number of elements of the FIFO queue to be created for
+                           buffering packets.
+        tx_sw_buffer_flushtime: Set the time before packets in the FIFO queue are flushed.
+        lkup_memory: Set the size of the noisy neighbor simulation memory buffer in MB to N.
+        lkup_num_reads: Set the size of the noisy neighbor simulation memory buffer in MB to N.
+        lkup_num_writes: Set the number of writes to be done in noisy neighbor simulation
+                         memory buffer to N.
+        lkup_num_reads_writes: Set the number of r/w accesses to be done in noisy neighbor
+                               simulation memory buffer to N.
+    """
+
+    _forward_mode: Literal["noisy"] = field(
+        default="noisy", init=False, metadata=Params.long("forward-mode")
+    )
+    forward_mode: (
+        Literal[
+            SimpleForwardingModes.io,
+            SimpleForwardingModes.mac,
+            SimpleForwardingModes.macswap,
+            SimpleForwardingModes.fivetswap,
+        ]
+        | None
+    ) = field(default=SimpleForwardingModes.io, metadata=Params.long("noisy-forward-mode"))
+    tx_sw_buffer_size: int | None = field(
+        default=None, metadata=Params.long("noisy-tx-sw-buffer-size")
+    )
+    tx_sw_buffer_flushtime: int | None = field(
+        default=None, metadata=Params.long("noisy-tx-sw-buffer-flushtime")
+    )
+    lkup_memory: int | None = field(default=None, metadata=Params.long("noisy-lkup-memory"))
+    lkup_num_reads: int | None = field(default=None, metadata=Params.long("noisy-lkup-num-reads"))
+    lkup_num_writes: int | None = field(default=None, metadata=Params.long("noisy-lkup-num-writes"))
+    lkup_num_reads_writes: int | None = field(
+        default=None, metadata=Params.long("noisy-lkup-num-reads-writes")
+    )
+
+
+@convert_str(hex_from_flag_value)
+@unique
+class HairpinMode(Flag):
+    """Flag representing the hairpin mode."""
+
+    #: Two hairpin ports loop.
+    TWO_PORTS_LOOP = 1 << 0
+    #: Two hairpin ports paired.
+    TWO_PORTS_PAIRED = 1 << 1
+    #: Explicit Tx flow rule.
+    EXPLICIT_TX_FLOW = 1 << 4
+    #: Force memory settings of hairpin RX queue.
+    FORCE_RX_QUEUE_MEM_SETTINGS = 1 << 8
+    #: Force memory settings of hairpin TX queue.
+    FORCE_TX_QUEUE_MEM_SETTINGS = 1 << 9
+    #: Hairpin RX queues will use locked device memory.
+    RX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 12
+    #: Hairpin RX queues will use RTE memory.
+    RX_QUEUE_USE_RTE_MEMORY = 1 << 13
+    #: Hairpin TX queues will use locked device memory.
+    TX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 16
+    #: Hairpin TX queues will use RTE memory.
+    TX_QUEUE_USE_RTE_MEMORY = 1 << 18
+
+
+@dataclass(kw_only=True)
+class RXRingParams(Params):
+    """Sets the RX ring parameters.
+
+    Attributes:
+        descriptors: Set the number of descriptors in the RX rings to N, where N > 0.
+        prefetch_threshold: Set the prefetch threshold register of RX rings to N, where N >= 0.
+        host_threshold: Set the host threshold register of RX rings to N, where N >= 0.
+        write_back_threshold: Set the write-back threshold register of RX rings to N, where N >= 0.
+        free_threshold: Set the free threshold of RX descriptors to N,
+                        where 0 <= N < value of ``-–rxd``.
+    """
+
+    descriptors: int | None = field(default=None, metadata=Params.long("rxd"))
+    prefetch_threshold: int | None = field(default=None, metadata=Params.long("rxpt"))
+    host_threshold: int | None = field(default=None, metadata=Params.long("rxht"))
+    write_back_threshold: int | None = field(default=None, metadata=Params.long("rxwt"))
+    free_threshold: int | None = field(default=None, metadata=Params.long("rxfreet"))
+
+
+@convert_str(hex_from_flag_value)
+@unique
+class RXMultiQueueMode(Flag):
+    """Flag representing the RX multi-queue mode."""
+
+    #:
+    RSS = 1 << 0
+    #:
+    DCB = 1 << 1
+    #:
+    VMDQ = 1 << 2
+
+
+@dataclass(kw_only=True)
+class TXRingParams(Params):
+    """Sets the TX ring parameters.
+
+    Attributes:
+        descriptors: Set the number of descriptors in the TX rings to N, where N > 0.
+        rs_bit_threshold: Set the transmit RS bit threshold of TX rings to N,
+                          where 0 <= N <= value of ``--txd``.
+        prefetch_threshold: Set the prefetch threshold register of TX rings to N, where N >= 0.
+        host_threshold: Set the host threshold register of TX rings to N, where N >= 0.
+        write_back_threshold: Set the write-back threshold register of TX rings to N, where N >= 0.
+        free_threshold: Set the transmit free threshold of TX rings to N,
+                        where 0 <= N <= value of ``--txd``.
+    """
+
+    descriptors: int | None = field(default=None, metadata=Params.long("txd"))
+    rs_bit_threshold: int | None = field(default=None, metadata=Params.long("txrst"))
+    prefetch_threshold: int | None = field(default=None, metadata=Params.long("txpt"))
+    host_threshold: int | None = field(default=None, metadata=Params.long("txht"))
+    write_back_threshold: int | None = field(default=None, metadata=Params.long("txwt"))
+    free_threshold: int | None = field(default=None, metadata=Params.long("txfreet"))
+
+
+class Event(StrEnum):
+    """Enum representing a testpmd event."""
+
+    #:
+    unknown = auto()
+    #:
+    queue_state = auto()
+    #:
+    vf_mbox = auto()
+    #:
+    macsec = auto()
+    #:
+    intr_lsc = auto()
+    #:
+    intr_rmv = auto()
+    #:
+    intr_reset = auto()
+    #:
+    dev_probed = auto()
+    #:
+    dev_released = auto()
+    #:
+    flow_aged = auto()
+    #:
+    err_recovering = auto()
+    #:
+    recovery_success = auto()
+    #:
+    recovery_failed = auto()
+    #:
+    all = auto()
+
+
+class SimpleMempoolAllocationMode(StrEnum):
+    """Enum representing simple mempool allocation modes."""
+
+    #: Create and populate mempool using native DPDK memory.
+    native = auto()
+    #: Create and populate mempool using externally and anonymously allocated area.
+    xmem = auto()
+    #: Create and populate mempool using externally and anonymously allocated hugepage area.
+    xmemhuge = auto()
+
+
+@dataclass(kw_only=True)
+class AnonMempoolAllocationMode(Params):
+    """Create mempool using native DPDK memory, but populate using anonymous memory.
+
+    Attributes:
+        no_iova_contig: Enables to create mempool which is not IOVA contiguous.
+    """
+
+    _mp_alloc: Literal["anon"] = field(default="anon", init=False, metadata=Params.long("mp-alloc"))
+    no_iova_contig: Switch = None
+
+
+@dataclass(slots=True, kw_only=True)
+class TestPmdParams(EalParams):
+    """The testpmd shell parameters.
+
+    Attributes:
+        interactive_mode: Runs testpmd in interactive mode.
+        auto_start: Start forwarding on initialization.
+        tx_first: Start forwarding, after sending a burst of packets first.
+        stats_period: Display statistics every ``PERIOD`` seconds, if interactive mode is disabled.
+                      The default value is 0, which means that the statistics will not be displayed.
+
+                      .. note:: This flag should be used only in non-interactive mode.
+        display_xstats: Display comma-separated list of extended statistics every ``PERIOD`` seconds
+                        as specified in ``--stats-period`` or when used with interactive commands
+                        that show Rx/Tx statistics (i.e. ‘show port stats’).
+        nb_cores: Set the number of forwarding cores, where 1 <= N <= “number of cores” or
+                  ``RTE_MAX_LCORE`` from the configuration file.
+        coremask: Set the bitmask of the cores running the packet forwarding test. The main
+                  lcore is reserved for command line parsing only and cannot be masked on for packet
+                  forwarding.
+        nb_ports: Set the number of forwarding ports, where 1 <= N <= “number of ports” on the board
+                  or ``RTE_MAX_ETHPORTS`` from the configuration file. The default value is the
+                  number of ports on the board.
+        port_topology: Set port topology, where mode is paired (the default), chained or loop.
+        portmask: Set the bitmask of the ports used by the packet forwarding test.
+        portlist: Set the forwarding ports based on the user input used by the packet forwarding
+                  test. ‘-‘ denotes a range of ports to set including the two specified port IDs ‘,’
+                  separates multiple port values. Possible examples like –portlist=0,1 or
+                  –portlist=0-2 or –portlist=0,1-2 etc.
+        numa: Enable/disable NUMA-aware allocation of RX/TX rings and of RX memory buffers (mbufs).
+        socket_num: Set the socket from which all memory is allocated in NUMA mode, where
+                    0 <= N < number of sockets on the board.
+        port_numa_config: Specify the socket on which the memory pool to be used by the port will be
+                          allocated.
+        ring_numa_config: Specify the socket on which the TX/RX rings for the port will be
+                          allocated. Where flag is 1 for RX, 2 for TX, and 3 for RX and TX.
+        total_num_mbufs: Set the number of mbufs to be allocated in the mbuf pools, where N > 1024.
+        mbuf_size: Set the data size of the mbufs used to N bytes, where N < 65536.
+                   If multiple mbuf-size values are specified the extra memory pools will be created
+                   for allocating mbufs to receive packets with buffer splitting features.
+        mbcache: Set the cache of mbuf memory pools to N, where 0 <= N <= 512.
+        max_pkt_len: Set the maximum packet size to N bytes, where N >= 64.
+        eth_peers_configfile: Use a configuration file containing the Ethernet addresses of
+                              the peer ports.
+        eth_peer: Set the MAC address XX:XX:XX:XX:XX:XX of the peer port N,
+                  where 0 <= N < RTE_MAX_ETHPORTS.
+        tx_ip: Set the source and destination IP address used when doing transmit only test.
+               The defaults address values are source 198.18.0.1 and destination 198.18.0.2.
+               These are special purpose addresses reserved for benchmarking (RFC 5735).
+        tx_udp: Set the source and destination UDP port number for transmit test only test.
+                The default port is the port 9 which is defined for the discard protocol (RFC 863).
+        enable_lro: Enable large receive offload.
+        max_lro_pkt_size: Set the maximum LRO aggregated packet size to N bytes, where N >= 64.
+        disable_crc_strip: Disable hardware CRC stripping.
+        enable_scatter: Enable scatter (multi-segment) RX.
+        enable_hw_vlan: Enable hardware VLAN.
+        enable_hw_vlan_filter: Enable hardware VLAN filter.
+        enable_hw_vlan_strip: Enable hardware VLAN strip.
+        enable_hw_vlan_extend: Enable hardware VLAN extend.
+        enable_hw_qinq_strip: Enable hardware QINQ strip.
+        pkt_drop_enabled: Enable per-queue packet drop for packets with no descriptors.
+        rss: Receive Side Scaling setting.
+        forward_mode: Set the forwarding mode.
+        hairpin_mode: Set the hairpin port configuration.
+        hairpin_queues: Set the number of hairpin queues per port to N, where 1 <= N <= 65535.
+        burst: Set the number of packets per burst to N, where 1 <= N <= 512.
+        enable_rx_cksum: Enable hardware RX checksum offload.
+        rx_queues: Set the number of RX queues per port to N, where 1 <= N <= 65535.
+        rx_ring: Set the RX rings parameters.
+        no_flush_rx: Don’t flush the RX streams before starting forwarding. Used mainly with
+                     the PCAP PMD.
+        rx_segments_offsets: Set the offsets of packet segments on receiving
+                             if split feature is engaged.
+        rx_segments_length: Set the length of segments to scatter packets on receiving
+                            if split feature is engaged.
+        multi_rx_mempool: Enable multiple mbuf pools per Rx queue.
+        rx_shared_queue: Create queues in shared Rx queue mode if device supports. Shared Rx queues
+                         are grouped per X ports. X defaults to UINT32_MAX, implies all ports join
+                         share group 1. Forwarding engine “shared-rxq” should be used for shared Rx
+                         queues. This engine does Rx only and update stream statistics accordingly.
+        rx_offloads: Set the bitmask of RX queue offloads.
+        rx_mq_mode: Set the RX multi queue mode which can be enabled.
+        tx_queues: Set the number of TX queues per port to N, where 1 <= N <= 65535.
+        tx_ring: Set the TX rings params.
+        tx_offloads: Set the hexadecimal bitmask of TX queue offloads.
+        eth_link_speed: Set a forced link speed to the ethernet port. E.g. 1000 for 1Gbps.
+        disable_link_check: Disable check on link status when starting/stopping ports.
+        disable_device_start: Do not automatically start all ports. This allows testing
+                              configuration of rx and tx queues before device is started
+                              for the first time.
+        no_lsc_interrupt: Disable LSC interrupts for all ports, even those supporting it.
+        no_rmv_interrupt: Disable RMV interrupts for all ports, even those supporting it.
+        bitrate_stats: Set the logical core N to perform bitrate calculation.
+        latencystats: Set the logical core N to perform latency and jitter calculations.
+        print_events: Enable printing the occurrence of the designated events.
+                      Using :attr:`TestPmdEvent.ALL` will enable all of them.
+        mask_events: Disable printing the occurrence of the designated events.
+                     Using :attr:`TestPmdEvent.ALL` will disable all of them.
+        flow_isolate_all: Providing this parameter requests flow API isolated mode on all ports at
+                          initialization time. It ensures all traffic is received through the
+                          configured flow rules only (see flow command). Ports that do not support
+                          this mode are automatically discarded.
+        disable_flow_flush: Disable port flow flush when stopping port.
+                            This allows testing keep flow rules or shared flow objects across
+                            restart.
+        hot_plug: Enable device event monitor mechanism for hotplug.
+        vxlan_gpe_port: Set the UDP port number of tunnel VXLAN-GPE to N.
+        geneve_parsed_port: Set the UDP port number that is used for parsing the GENEVE protocol
+                            to N. HW may be configured with another tunnel Geneve port.
+        lock_all_memory: Enable/disable locking all memory. Disabled by default.
+        mempool_allocation_mode: Set mempool allocation mode.
+        record_core_cycles: Enable measurement of CPU cycles per packet.
+        record_burst_status: Enable display of RX and TX burst stats.
+    """
+
+    interactive_mode: Switch = field(default=True, metadata=Params.short("i"))
+    auto_start: Switch = field(default=None, metadata=Params.short("a"))
+    tx_first: Switch = None
+    stats_period: int | None = None
+    display_xstats: list[str] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    nb_cores: int | None = None
+    coremask: int | None = field(default=None, metadata=Params.convert_value(hex))
+    nb_ports: int | None = None
+    port_topology: PortTopology | None = PortTopology.paired
+    portmask: int | None = field(default=None, metadata=Params.convert_value(hex))
+    portlist: str | None = None  # TODO: can be ranges 0,1-3
+
+    numa: YesNoSwitch = True
+    socket_num: int | None = None
+    port_numa_config: list[PortNUMAConfig] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    ring_numa_config: list[RingNUMAConfig] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    total_num_mbufs: int | None = None
+    mbuf_size: list[int] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    mbcache: int | None = None
+    max_pkt_len: int | None = None
+    eth_peers_configfile: PurePath | None = None
+    eth_peer: list[EthPeer] | None = field(default=None, metadata=Params.multiple())
+    tx_ip: TxIPAddrPair | None = TxIPAddrPair(source_ip="198.18.0.1", dest_ip="198.18.0.2")
+    tx_udp: TxUDPPortPair | None = TxUDPPortPair(9)
+    enable_lro: Switch = None
+    max_lro_pkt_size: int | None = None
+    disable_crc_strip: Switch = None
+    enable_scatter: Switch = None
+    enable_hw_vlan: Switch = None
+    enable_hw_vlan_filter: Switch = None
+    enable_hw_vlan_strip: Switch = None
+    enable_hw_vlan_extend: Switch = None
+    enable_hw_qinq_strip: Switch = None
+    pkt_drop_enabled: Switch = field(default=None, metadata=Params.long("enable-drop-en"))
+    rss: RSSSetting | None = None
+    forward_mode: (
+        SimpleForwardingModes
+        | FlowGenForwardingMode
+        | TXOnlyForwardingMode
+        | NoisyForwardingMode
+        | None
+    ) = SimpleForwardingModes.io
+    hairpin_mode: HairpinMode | None = HairpinMode(0)
+    hairpin_queues: int | None = field(default=None, metadata=Params.long("hairpinq"))
+    burst: int | None = None
+    enable_rx_cksum: Switch = None
+
+    rx_queues: int | None = field(default=None, metadata=Params.long("rxq"))
+    rx_ring: RXRingParams | None = None
+    no_flush_rx: Switch = None
+    rx_segments_offsets: list[int] | None = field(
+        default=None, metadata=Params.long("rxoffs") | Params.convert_value(comma_separated)
+    )
+    rx_segments_length: list[int] | None = field(
+        default=None, metadata=Params.long("rxpkts") | Params.convert_value(comma_separated)
+    )
+    multi_rx_mempool: Switch = None
+    rx_shared_queue: Switch | int = field(default=None, metadata=Params.long("rxq-share"))
+    rx_offloads: int | None = field(default=None, metadata=Params.convert_value(hex))
+    rx_mq_mode: RXMultiQueueMode | None = (
+        RXMultiQueueMode.DCB | RXMultiQueueMode.RSS | RXMultiQueueMode.VMDQ
+    )
+
+    tx_queues: int | None = field(default=None, metadata=Params.long("txq"))
+    tx_ring: TXRingParams | None = None
+    tx_offloads: int | None = field(default=None, metadata=Params.convert_value(hex))
+
+    eth_link_speed: int | None = None
+    disable_link_check: Switch = None
+    disable_device_start: Switch = None
+    no_lsc_interrupt: Switch = None
+    no_rmv_interrupt: Switch = None
+    bitrate_stats: int | None = None
+    latencystats: int | None = None
+    print_events: list[Event] | None = field(
+        default=None, metadata=Params.multiple() | Params.long("print-event")
+    )
+    mask_events: list[Event] | None = field(
+        default_factory=lambda: [Event.intr_lsc],
+        metadata=Params.multiple() | Params.long("mask-event"),
+    )
+
+    flow_isolate_all: Switch = None
+    disable_flow_flush: Switch = None
+
+    hot_plug: Switch = None
+    vxlan_gpe_port: int | None = None
+    geneve_parsed_port: int | None = None
+    lock_all_memory: YesNoSwitch = field(default=False, metadata=Params.long("mlockall"))
+    mempool_allocation_mode: SimpleMempoolAllocationMode | AnonMempoolAllocationMode | None = field(
+        default=None, metadata=Params.long("mp-alloc")
+    )
+    record_core_cycles: Switch = None
+    record_burst_status: Switch = None
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index 841d456a2f..ef3f23c582 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright(c) 2023 University of New Hampshire
 # Copyright(c) 2023 PANTHEON.tech s.r.o.
+# Copyright(c) 2024 Arm Limited
 
 """Testpmd interactive shell.
 
@@ -16,14 +17,12 @@
 """
 
 import time
-from enum import auto
 from pathlib import PurePath
 from typing import Callable, ClassVar
 
 from framework.exception import InteractiveCommandExecutionError
-from framework.params.eal import EalParams
+from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
 from framework.settings import SETTINGS
-from framework.utils import StrEnum
 
 from .interactive_shell import InteractiveShell
 
@@ -50,37 +49,6 @@ def __str__(self) -> str:
         return self.pci_address
 
 
-class TestPmdForwardingModes(StrEnum):
-    r"""The supported packet forwarding modes for :class:`~TestPmdShell`\s."""
-
-    #:
-    io = auto()
-    #:
-    mac = auto()
-    #:
-    macswap = auto()
-    #:
-    flowgen = auto()
-    #:
-    rxonly = auto()
-    #:
-    txonly = auto()
-    #:
-    csum = auto()
-    #:
-    icmpecho = auto()
-    #:
-    ieee1588 = auto()
-    #:
-    noisy = auto()
-    #:
-    fivetswap = "5tswap"
-    #:
-    shared_rxq = "shared-rxq"
-    #:
-    recycle_mbufs = auto()
-
-
 class TestPmdShell(InteractiveShell):
     """Testpmd interactive shell.
 
@@ -119,9 +87,7 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
         Also find the number of pci addresses which were allowed on the command line when the app
         was started.
         """
-        self._app_params += " -i --mask-event intr_lsc"
-
-        assert isinstance(self._app_params, EalParams)
+        assert isinstance(self._app_params, TestPmdParams)
 
         self.number_of_ports = (
             len(self._app_params.ports) if self._app_params.ports is not None else 0
@@ -213,7 +179,7 @@ def wait_link_status_up(self, port_id: int, timeout=SETTINGS.timeout) -> bool:
             self._logger.error(f"The link for port {port_id} did not come up in the given timeout.")
         return "Link status: up" in port_info
 
-    def set_forward_mode(self, mode: TestPmdForwardingModes, verify: bool = True):
+    def set_forward_mode(self, mode: SimpleForwardingModes, verify: bool = True):
         """Set packet forwarding mode.
 
         Args:
diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index c6e93839cb..578b5a4318 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -23,7 +23,8 @@
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
 from framework.params import Params
-from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
+from framework.params.testpmd import SimpleForwardingModes
+from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.test_suite import TestSuite
 
 
@@ -113,7 +114,7 @@ def pmd_scatter(self, mbsize: int) -> None:
             ),
             privileged=True,
         )
-        testpmd.set_forward_mode(TestPmdForwardingModes.mac)
+        testpmd.set_forward_mode(SimpleForwardingModes.mac)
         testpmd.start()
 
         for offset in [-1, 0, 1, 4, 5]:
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v3 6/8] dts: use testpmd params for scatter test suite
  2024-05-30 15:24 ` [PATCH v3 " Luca Vizzarro
                     ` (4 preceding siblings ...)
  2024-05-30 15:25   ` [PATCH v3 5/8] dts: add testpmd shell params Luca Vizzarro
@ 2024-05-30 15:25   ` Luca Vizzarro
  2024-05-30 20:13     ` Jeremy Spewock
                       ` (2 more replies)
  2024-05-30 15:25   ` [PATCH v3 7/8] dts: rework interactive shells Luca Vizzarro
  2024-05-30 15:25   ` [PATCH v3 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
  7 siblings, 3 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-05-30 15:25 UTC (permalink / raw)
  To: dev; +Cc: Juraj Linkeš, Jeremy Spewock, Luca Vizzarro, Paul Szczepanek

Update the buffer scatter test suite to use TestPmdParameters
instead of the StrParams implementation.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 dts/tests/TestSuite_pmd_buffer_scatter.py | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index 578b5a4318..6d206c1a40 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -16,14 +16,14 @@
 """
 
 import struct
+from dataclasses import asdict
 
 from scapy.layers.inet import IP  # type: ignore[import-untyped]
 from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
 from scapy.packet import Raw  # type: ignore[import-untyped]
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
-from framework.params import Params
-from framework.params.testpmd import SimpleForwardingModes
+from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
 from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.test_suite import TestSuite
 
@@ -105,16 +105,16 @@ def pmd_scatter(self, mbsize: int) -> None:
         """
         testpmd = self.sut_node.create_interactive_shell(
             TestPmdShell,
-            app_params=Params.from_str(
-                "--mbcache=200 "
-                f"--mbuf-size={mbsize} "
-                "--max-pkt-len=9000 "
-                "--port-topology=paired "
-                "--tx-offloads=0x00008000"
+            app_params=TestPmdParams(
+                forward_mode=SimpleForwardingModes.mac,
+                mbcache=200,
+                mbuf_size=[mbsize],
+                max_pkt_len=9000,
+                tx_offloads=0x00008000,
+                **asdict(self.sut_node.create_eal_parameters()),
             ),
             privileged=True,
         )
-        testpmd.set_forward_mode(SimpleForwardingModes.mac)
         testpmd.start()
 
         for offset in [-1, 0, 1, 4, 5]:
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v3 7/8] dts: rework interactive shells
  2024-05-30 15:24 ` [PATCH v3 " Luca Vizzarro
                     ` (5 preceding siblings ...)
  2024-05-30 15:25   ` [PATCH v3 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
@ 2024-05-30 15:25   ` Luca Vizzarro
  2024-05-30 20:13     ` Jeremy Spewock
                       ` (2 more replies)
  2024-05-30 15:25   ` [PATCH v3 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
  7 siblings, 3 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-05-30 15:25 UTC (permalink / raw)
  To: dev; +Cc: Juraj Linkeš, Jeremy Spewock, Luca Vizzarro, Paul Szczepanek

The way nodes and interactive shells interact makes it difficult to
develop for static type checking and hinting. The current system relies
on a top-down approach, attempting to give a generic interface to the
test developer, hiding the interaction of concrete shell classes as much
as possible. When working with strong typing this approach is not ideal,
as Python's implementation of generics is still rudimentary.

This rework reverses the tests interaction to a bottom-up approach,
allowing the test developer to call concrete shell classes directly,
and let them ingest nodes independently. While also re-enforcing type
checking and making the code easier to read.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 dts/framework/params/eal.py                   |   6 +-
 dts/framework/remote_session/dpdk_shell.py    | 104 ++++++++++++++++
 .../remote_session/interactive_shell.py       |  74 +++++++-----
 dts/framework/remote_session/python_shell.py  |   4 +-
 dts/framework/remote_session/testpmd_shell.py |  64 +++++-----
 dts/framework/testbed_model/node.py           |  36 +-----
 dts/framework/testbed_model/os_session.py     |  36 +-----
 dts/framework/testbed_model/sut_node.py       | 112 +-----------------
 .../testbed_model/traffic_generator/scapy.py  |   4 +-
 dts/tests/TestSuite_hello_world.py            |   7 +-
 dts/tests/TestSuite_pmd_buffer_scatter.py     |  21 ++--
 dts/tests/TestSuite_smoke_tests.py            |   2 +-
 12 files changed, 200 insertions(+), 270 deletions(-)
 create mode 100644 dts/framework/remote_session/dpdk_shell.py

diff --git a/dts/framework/params/eal.py b/dts/framework/params/eal.py
index bbdbc8f334..8d7766fefc 100644
--- a/dts/framework/params/eal.py
+++ b/dts/framework/params/eal.py
@@ -35,9 +35,9 @@ class EalParams(Params):
                 ``other_eal_param='--single-file-segments'``
     """
 
-    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
-    memory_channels: int = field(metadata=Params.short("n"))
-    prefix: str = field(metadata=Params.long("file-prefix"))
+    lcore_list: LogicalCoreList | None = field(default=None, metadata=Params.short("l"))
+    memory_channels: int | None = field(default=None, metadata=Params.short("n"))
+    prefix: str = field(default="dpdk", metadata=Params.long("file-prefix"))
     no_pci: Switch = None
     vdevs: list[VirtualDevice] | None = field(
         default=None, metadata=Params.multiple() | Params.long("vdev")
diff --git a/dts/framework/remote_session/dpdk_shell.py b/dts/framework/remote_session/dpdk_shell.py
new file mode 100644
index 0000000000..25e3df4eaa
--- /dev/null
+++ b/dts/framework/remote_session/dpdk_shell.py
@@ -0,0 +1,104 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""DPDK-based interactive shell.
+
+Provides a base class to create interactive shells based on DPDK.
+"""
+
+
+from abc import ABC
+
+from framework.params.eal import EalParams
+from framework.remote_session.interactive_shell import InteractiveShell
+from framework.settings import SETTINGS
+from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
+from framework.testbed_model.sut_node import SutNode
+
+
+def compute_eal_params(
+    node: SutNode,
+    params: EalParams | None = None,
+    lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
+    ascending_cores: bool = True,
+    append_prefix_timestamp: bool = True,
+) -> EalParams:
+    """Compute EAL parameters based on the node's specifications.
+
+    Args:
+        node: The SUT node to compute the values for.
+        params: The EalParams object to amend, if set to None a new object is created and returned.
+        lcore_filter_specifier: A number of lcores/cores/sockets to use
+            or a list of lcore ids to use.
+            The default will select one lcore for each of two cores
+            on one socket, in ascending order of core ids.
+        ascending_cores: Sort cores in ascending order (lowest to highest IDs).
+            If :data:`False`, sort in descending order.
+        append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.
+    """
+    if params is None:
+        params = EalParams()
+
+    if params.lcore_list is None:
+        params.lcore_list = LogicalCoreList(
+            node.filter_lcores(lcore_filter_specifier, ascending_cores)
+        )
+
+    prefix = params.prefix
+    if append_prefix_timestamp:
+        prefix = f"{prefix}_{node._dpdk_timestamp}"
+    prefix = node.main_session.get_dpdk_file_prefix(prefix)
+    if prefix:
+        node._dpdk_prefix_list.append(prefix)
+    params.prefix = prefix
+
+    if params.ports is None:
+        params.ports = node.ports
+
+    return params
+
+
+class DPDKShell(InteractiveShell, ABC):
+    """The base class for managing DPDK-based interactive shells.
+
+    This class shouldn't be instantiated directly, but instead be extended.
+    It automatically injects computed EAL parameters based on the node in the
+    supplied app parameters.
+    """
+
+    _node: SutNode
+    _app_params: EalParams
+
+    _lcore_filter_specifier: LogicalCoreCount | LogicalCoreList
+    _ascending_cores: bool
+    _append_prefix_timestamp: bool
+
+    def __init__(
+        self,
+        node: SutNode,
+        app_params: EalParams,
+        privileged: bool = True,
+        timeout: float = SETTINGS.timeout,
+        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
+        ascending_cores: bool = True,
+        append_prefix_timestamp: bool = True,
+        start_on_init: bool = True,
+    ) -> None:
+        """Overrides :meth:`~.interactive_shell.InteractiveShell.__init__`."""
+        self._lcore_filter_specifier = lcore_filter_specifier
+        self._ascending_cores = ascending_cores
+        self._append_prefix_timestamp = append_prefix_timestamp
+
+        super().__init__(node, app_params, privileged, timeout, start_on_init)
+
+    def _post_init(self):
+        """Computes EAL params based on the node capabilities before start."""
+        self._app_params = compute_eal_params(
+            self._node,
+            self._app_params,
+            self._lcore_filter_specifier,
+            self._ascending_cores,
+            self._append_prefix_timestamp,
+        )
+
+        self._update_path(self._node.remote_dpdk_build_dir.joinpath(self.path))
diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
index 9da66d1c7e..4be7966672 100644
--- a/dts/framework/remote_session/interactive_shell.py
+++ b/dts/framework/remote_session/interactive_shell.py
@@ -17,13 +17,14 @@
 
 from abc import ABC
 from pathlib import PurePath
-from typing import Callable, ClassVar
+from typing import ClassVar
 
-from paramiko import Channel, SSHClient, channel  # type: ignore[import-untyped]
+from paramiko import Channel, channel  # type: ignore[import-untyped]
 
 from framework.logger import DTSLogger
 from framework.params import Params
 from framework.settings import SETTINGS
+from framework.testbed_model.node import Node
 
 
 class InteractiveShell(ABC):
@@ -36,13 +37,14 @@ class InteractiveShell(ABC):
     session.
     """
 
-    _interactive_session: SSHClient
+    _node: Node
     _stdin: channel.ChannelStdinFile
     _stdout: channel.ChannelFile
     _ssh_channel: Channel
     _logger: DTSLogger
     _timeout: float
     _app_params: Params
+    _privileged: bool
 
     #: Prompt to expect at the end of output when sending a command.
     #: This is often overridden by subclasses.
@@ -56,55 +58,63 @@ class InteractiveShell(ABC):
     #: Path to the executable to start the interactive application.
     path: ClassVar[PurePath]
 
-    #: Whether this application is a DPDK app. If it is, the build directory
-    #: for DPDK on the node will be prepended to the path to the executable.
-    dpdk_app: ClassVar[bool] = False
-
     def __init__(
         self,
-        interactive_session: SSHClient,
-        logger: DTSLogger,
-        get_privileged_command: Callable[[str], str] | None,
+        node: Node,
         app_params: Params = Params(),
+        privileged: bool = False,
         timeout: float = SETTINGS.timeout,
+        start_on_init: bool = True,
     ) -> None:
         """Create an SSH channel during initialization.
 
         Args:
-            interactive_session: The SSH session dedicated to interactive shells.
-            logger: The logger instance this session will use.
-            get_privileged_command: A method for modifying a command to allow it to use
-                elevated privileges. If :data:`None`, the application will not be started
-                with elevated privileges.
+            node: The node on which to run start the interactive shell.
             app_params: The command line parameters to be passed to the application on startup.
+            privileged: Enables the shell to run as superuser.
             timeout: The timeout used for the SSH channel that is dedicated to this interactive
                 shell. This timeout is for collecting output, so if reading from the buffer
                 and no output is gathered within the timeout, an exception is thrown.
+            start_on_init: Start interactive shell automatically after object initialisation.
         """
-        self._interactive_session = interactive_session
-        self._ssh_channel = self._interactive_session.invoke_shell()
+        self._node = node
+        self._logger = node._logger
+        self._app_params = app_params
+        self._privileged = privileged
+        self._timeout = timeout
+        # Ensure path is properly formatted for the host
+        self._update_path(self._node.main_session.join_remote_path(self.path))
+
+        self._post_init()
+
+        if start_on_init:
+            self.start_application()
+
+    def _post_init(self):
+        """Overridable. Method called after the object init and before application start."""
+
+    def _setup_ssh_channel(self):
+        self._ssh_channel = self._node.main_session.interactive_session.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.settimeout(self._timeout)
         self._ssh_channel.set_combine_stderr(True)  # combines stdout and stderr streams
-        self._logger = logger
-        self._timeout = timeout
-        self._app_params = app_params
-        self._start_application(get_privileged_command)
 
-    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
+    def _make_start_command(self) -> str:
+        """Makes the command that starts the interactive shell."""
+        return f"{self.path} {self._app_params or ''}"
+
+    def start_application(self) -> None:
         """Starts a new interactive application based on the path to the app.
 
         This method is often overridden by subclasses as their process for
         starting may look different.
-
-        Args:
-            get_privileged_command: A function (but could be any callable) that produces
-                the version of the command with elevated privileges.
         """
-        start_command = f"{self.path} {self._app_params}"
-        if get_privileged_command is not None:
-            start_command = get_privileged_command(start_command)
+        self._setup_ssh_channel()
+
+        start_command = self._make_start_command()
+        if self._privileged:
+            start_command = self._node.main_session._get_privileged_command(start_command)
         self.send_command(start_command)
 
     def send_command(self, command: str, prompt: str | None = None) -> str:
@@ -150,3 +160,7 @@ def close(self) -> None:
     def __del__(self) -> None:
         """Make sure the session is properly closed before deleting the object."""
         self.close()
+
+    @classmethod
+    def _update_path(cls, path: PurePath) -> None:
+        cls.path = path
diff --git a/dts/framework/remote_session/python_shell.py b/dts/framework/remote_session/python_shell.py
index ccfd3783e8..953ed100df 100644
--- a/dts/framework/remote_session/python_shell.py
+++ b/dts/framework/remote_session/python_shell.py
@@ -6,9 +6,7 @@
 Typical usage example in a TestSuite::
 
     from framework.remote_session import PythonShell
-    python_shell = self.tg_node.create_interactive_shell(
-        PythonShell, timeout=5, privileged=True
-    )
+    python_shell = PythonShell(self.tg_node, timeout=5, privileged=True)
     python_shell.send_command("print('Hello World')")
     python_shell.close()
 """
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index ef3f23c582..39985000b9 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -7,9 +7,7 @@
 
 Typical usage example in a TestSuite::
 
-    testpmd_shell = self.sut_node.create_interactive_shell(
-            TestPmdShell, privileged=True
-        )
+    testpmd_shell = TestPmdShell(self.sut_node)
     devices = testpmd_shell.get_devices()
     for device in devices:
         print(device)
@@ -18,13 +16,14 @@
 
 import time
 from pathlib import PurePath
-from typing import Callable, ClassVar
+from typing import ClassVar
 
 from framework.exception import InteractiveCommandExecutionError
 from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
+from framework.remote_session.dpdk_shell import DPDKShell
 from framework.settings import SETTINGS
-
-from .interactive_shell import InteractiveShell
+from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
+from framework.testbed_model.sut_node import SutNode
 
 
 class TestPmdDevice(object):
@@ -49,52 +48,48 @@ def __str__(self) -> str:
         return self.pci_address
 
 
-class TestPmdShell(InteractiveShell):
+class TestPmdShell(DPDKShell):
     """Testpmd interactive shell.
 
     The testpmd shell users should never use
     the :meth:`~.interactive_shell.InteractiveShell.send_command` method directly, but rather
     call specialized methods. If there isn't one that satisfies a need, it should be added.
-
-    Attributes:
-        number_of_ports: The number of ports which were allowed on the command-line when testpmd
-            was started.
     """
 
-    number_of_ports: int
+    _app_params: TestPmdParams
 
     #: The path to the testpmd executable.
     path: ClassVar[PurePath] = PurePath("app", "dpdk-testpmd")
 
-    #: Flag this as a DPDK app so that it's clear this is not a system app and
-    #: needs to be looked in a specific path.
-    dpdk_app: ClassVar[bool] = True
-
     #: The testpmd's prompt.
     _default_prompt: ClassVar[str] = "testpmd>"
 
     #: This forces the prompt to appear after sending a command.
     _command_extra_chars: ClassVar[str] = "\n"
 
-    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
-        """Overrides :meth:`~.interactive_shell._start_application`.
-
-        Add flags for starting testpmd in interactive mode and disabling messages for link state
-        change events before starting the application. Link state is verified before starting
-        packet forwarding and the messages create unexpected newlines in the terminal which
-        complicates output collection.
-
-        Also find the number of pci addresses which were allowed on the command line when the app
-        was started.
-        """
-        assert isinstance(self._app_params, TestPmdParams)
-
-        self.number_of_ports = (
-            len(self._app_params.ports) if self._app_params.ports is not None else 0
+    def __init__(
+        self,
+        node: SutNode,
+        privileged: bool = True,
+        timeout: float = SETTINGS.timeout,
+        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
+        ascending_cores: bool = True,
+        append_prefix_timestamp: bool = True,
+        start_on_init: bool = True,
+        **app_params,
+    ) -> None:
+        """Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs."""
+        super().__init__(
+            node,
+            TestPmdParams(**app_params),
+            privileged,
+            timeout,
+            lcore_filter_specifier,
+            ascending_cores,
+            append_prefix_timestamp,
+            start_on_init,
         )
 
-        super()._start_application(get_privileged_command)
-
     def start(self, verify: bool = True) -> None:
         """Start packet forwarding with the current configuration.
 
@@ -114,7 +109,8 @@ def start(self, verify: bool = True) -> None:
                 self._logger.debug(f"Failed to start packet forwarding: \n{start_cmd_output}")
                 raise InteractiveCommandExecutionError("Testpmd failed to start packet forwarding.")
 
-            for port_id in range(self.number_of_ports):
+            number_of_ports = len(self._app_params.ports or [])
+            for port_id in range(number_of_ports):
                 if not self.wait_link_status_up(port_id):
                     raise InteractiveCommandExecutionError(
                         "Not all ports came up after starting packet forwarding in testpmd."
diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
index 6af4f25a3c..88395faabe 100644
--- a/dts/framework/testbed_model/node.py
+++ b/dts/framework/testbed_model/node.py
@@ -15,7 +15,7 @@
 
 from abc import ABC
 from ipaddress import IPv4Interface, IPv6Interface
-from typing import Any, Callable, Type, Union
+from typing import Any, Callable, Union
 
 from framework.config import (
     OS,
@@ -25,7 +25,6 @@
 )
 from framework.exception import ConfigurationError
 from framework.logger import DTSLogger, get_dts_logger
-from framework.params import Params
 from framework.settings import SETTINGS
 
 from .cpu import (
@@ -36,7 +35,7 @@
     lcore_filter,
 )
 from .linux_session import LinuxSession
-from .os_session import InteractiveShellType, OSSession
+from .os_session import OSSession
 from .port import Port
 from .virtual_device import VirtualDevice
 
@@ -196,37 +195,6 @@ def create_session(self, name: str) -> OSSession:
         self._other_sessions.append(connection)
         return connection
 
-    def create_interactive_shell(
-        self,
-        shell_cls: Type[InteractiveShellType],
-        timeout: float = SETTINGS.timeout,
-        privileged: bool = False,
-        app_params: Params = Params(),
-    ) -> InteractiveShellType:
-        """Factory for interactive session handlers.
-
-        Instantiate `shell_cls` according to the remote OS specifics.
-
-        Args:
-            shell_cls: The class of the shell.
-            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.
-            privileged: Whether to run the shell with administrative privileges.
-            app_args: The arguments to be passed to the application.
-
-        Returns:
-            An instance of the desired interactive application shell.
-        """
-        if not shell_cls.dpdk_app:
-            shell_cls.path = self.main_session.join_remote_path(shell_cls.path)
-
-        return self.main_session.create_interactive_shell(
-            shell_cls,
-            timeout,
-            privileged,
-            app_params,
-        )
-
     def filter_lcores(
         self,
         filter_specifier: LogicalCoreCount | LogicalCoreList,
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
index e5f5fcbe0e..e7e6c9d670 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -26,18 +26,16 @@
 from collections.abc import Iterable
 from ipaddress import IPv4Interface, IPv6Interface
 from pathlib import PurePath
-from typing import Type, TypeVar, Union
+from typing import Union
 
 from framework.config import Architecture, NodeConfiguration, NodeInfo
 from framework.logger import DTSLogger
-from framework.params import Params
 from framework.remote_session import (
     InteractiveRemoteSession,
     RemoteSession,
     create_interactive_session,
     create_remote_session,
 )
-from framework.remote_session.interactive_shell import InteractiveShell
 from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
@@ -45,8 +43,6 @@
 from .cpu import LogicalCore
 from .port import Port
 
-InteractiveShellType = TypeVar("InteractiveShellType", bound=InteractiveShell)
-
 
 class OSSession(ABC):
     """OS-unaware to OS-aware translation API definition.
@@ -131,36 +127,6 @@ def send_command(
 
         return self.remote_session.send_command(command, timeout, verify, env)
 
-    def create_interactive_shell(
-        self,
-        shell_cls: Type[InteractiveShellType],
-        timeout: float,
-        privileged: bool,
-        app_args: Params,
-    ) -> InteractiveShellType:
-        """Factory for interactive session handlers.
-
-        Instantiate `shell_cls` according to the remote OS specifics.
-
-        Args:
-            shell_cls: The class of the shell.
-            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.
-            privileged: Whether to run the shell with administrative privileges.
-            app_args: The arguments to be passed to the application.
-
-        Returns:
-            An instance of the desired interactive application shell.
-        """
-        return shell_cls(
-            self.interactive_session.session,
-            self._logger,
-            self._get_privileged_command if privileged else None,
-            app_args,
-            timeout,
-        )
-
     @staticmethod
     @abstractmethod
     def _get_privileged_command(command: str) -> str:
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index 83ad06ae2d..727170b7fc 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -16,7 +16,6 @@
 import tarfile
 import time
 from pathlib import PurePath
-from typing import Type
 
 from framework.config import (
     BuildTargetConfiguration,
@@ -24,17 +23,13 @@
     NodeInfo,
     SutNodeConfiguration,
 )
-from framework.params import Params, Switch
 from framework.params.eal import EalParams
 from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
 
-from .cpu import LogicalCoreCount, LogicalCoreList
 from .node import Node
-from .os_session import InteractiveShellType, OSSession
-from .port import Port
-from .virtual_device import VirtualDevice
+from .os_session import OSSession
 
 
 class SutNode(Node):
@@ -289,68 +284,6 @@ def kill_cleanup_dpdk_apps(self) -> None:
             self._dpdk_kill_session = self.create_session("dpdk_kill")
         self._dpdk_prefix_list = []
 
-    def create_eal_parameters(
-        self,
-        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
-        ascending_cores: bool = True,
-        prefix: str = "dpdk",
-        append_prefix_timestamp: bool = True,
-        no_pci: Switch = None,
-        vdevs: list[VirtualDevice] | None = None,
-        ports: list[Port] | None = None,
-        other_eal_param: str = "",
-    ) -> EalParams:
-        """Compose the EAL parameters.
-
-        Process the list of cores and the DPDK prefix and pass that along with
-        the rest of the arguments.
-
-        Args:
-            lcore_filter_specifier: A number of lcores/cores/sockets to use
-                or a list of lcore ids to use.
-                The default will select one lcore for each of two cores
-                on one socket, in ascending order of core ids.
-            ascending_cores: Sort cores in ascending order (lowest to highest IDs).
-                If :data:`False`, sort in descending order.
-            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
-            append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.
-            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
-            vdevs: Virtual devices, e.g.::
-
-                vdevs=[
-                    VirtualDevice('net_ring0'),
-                    VirtualDevice('net_ring1')
-                ]
-            ports: The list of ports to allow. If :data:`None`, all ports listed in `self.ports`
-                will be allowed.
-            other_eal_param: user defined DPDK EAL parameters, e.g.:
-                ``other_eal_param='--single-file-segments'``.
-
-        Returns:
-            An EAL param string, such as
-            ``-c 0xf -a 0000:88:00.0 --file-prefix=dpdk_1112_20190809143420``.
-        """
-        lcore_list = LogicalCoreList(self.filter_lcores(lcore_filter_specifier, ascending_cores))
-
-        if append_prefix_timestamp:
-            prefix = f"{prefix}_{self._dpdk_timestamp}"
-        prefix = self.main_session.get_dpdk_file_prefix(prefix)
-        if prefix:
-            self._dpdk_prefix_list.append(prefix)
-
-        if ports is None:
-            ports = self.ports
-
-        return EalParams(
-            lcore_list=lcore_list,
-            memory_channels=self.config.memory_channels,
-            prefix=prefix,
-            no_pci=no_pci,
-            vdevs=vdevs,
-            ports=ports,
-            other_eal_param=Params.from_str(other_eal_param),
-        )
-
     def run_dpdk_app(
         self, app_path: PurePath, eal_params: EalParams, timeout: float = 30
     ) -> CommandResult:
@@ -379,49 +312,6 @@ def configure_ipv4_forwarding(self, enable: bool) -> None:
         """
         self.main_session.configure_ipv4_forwarding(enable)
 
-    def create_interactive_shell(
-        self,
-        shell_cls: Type[InteractiveShellType],
-        timeout: float = SETTINGS.timeout,
-        privileged: bool = False,
-        app_params: Params = Params(),
-        eal_params: EalParams | None = None,
-    ) -> InteractiveShellType:
-        """Extend the factory for interactive session handlers.
-
-        The extensions are SUT node specific:
-
-            * The default for `eal_parameters`,
-            * The interactive shell path `shell_cls.path` is prepended with path to the remote
-              DPDK build directory for DPDK apps.
-
-        Args:
-            shell_cls: The class of the shell.
-            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.
-            privileged: Whether to run the shell with administrative privileges.
-            app_params: The parameters to be passed to the application.
-            eal_params: List of EAL parameters to use to launch the app. If this
-                isn't provided or an empty string is passed, it will default to calling
-                :meth:`create_eal_parameters`.
-
-        Returns:
-            An instance of the desired interactive application shell.
-        """
-        # We need to append the build directory and add EAL parameters for DPDK apps
-        if shell_cls.dpdk_app:
-            if eal_params is None:
-                eal_params = self.create_eal_parameters()
-            eal_params.append_str(str(app_params))
-            app_params = eal_params
-
-            shell_cls.path = self.main_session.join_remote_path(
-                self.remote_dpdk_build_dir, shell_cls.path
-            )
-
-        return super().create_interactive_shell(shell_cls, timeout, privileged, app_params)
-
     def bind_ports_to_driver(self, for_dpdk: bool = True) -> None:
         """Bind all ports on the SUT to a driver.
 
diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py
index 7bc1c2cc08..bf58ad1c5e 100644
--- a/dts/framework/testbed_model/traffic_generator/scapy.py
+++ b/dts/framework/testbed_model/traffic_generator/scapy.py
@@ -217,9 +217,7 @@ def __init__(self, tg_node: Node, config: ScapyTrafficGeneratorConfig):
             self._tg_node.config.os == OS.linux
         ), "Linux is the only supported OS for scapy traffic generation"
 
-        self.session = self._tg_node.create_interactive_shell(
-            PythonShell, timeout=5, privileged=True
-        )
+        self.session = PythonShell(self._tg_node, timeout=5, privileged=True)
 
         # import libs in remote python console
         for import_statement in SCAPY_RPC_SERVER_IMPORTS:
diff --git a/dts/tests/TestSuite_hello_world.py b/dts/tests/TestSuite_hello_world.py
index 0d6995f260..d958f99030 100644
--- a/dts/tests/TestSuite_hello_world.py
+++ b/dts/tests/TestSuite_hello_world.py
@@ -7,6 +7,7 @@
 No other EAL parameters apart from cores are used.
 """
 
+from framework.remote_session.dpdk_shell import compute_eal_params
 from framework.test_suite import TestSuite
 from framework.testbed_model.cpu import (
     LogicalCoreCount,
@@ -38,7 +39,7 @@ def test_hello_world_single_core(self) -> None:
         # get the first usable core
         lcore_amount = LogicalCoreCount(1, 1, 1)
         lcores = LogicalCoreCountFilter(self.sut_node.lcores, lcore_amount).filter()
-        eal_para = self.sut_node.create_eal_parameters(lcore_filter_specifier=lcore_amount)
+        eal_para = compute_eal_params(self.sut_node, lcore_filter_specifier=lcore_amount)
         result = self.sut_node.run_dpdk_app(self.app_helloworld_path, eal_para)
         self.verify(
             f"hello from core {int(lcores[0])}" in result.stdout,
@@ -55,8 +56,8 @@ def test_hello_world_all_cores(self) -> None:
             "hello from core <core_id>"
         """
         # get the maximum logical core number
-        eal_para = self.sut_node.create_eal_parameters(
-            lcore_filter_specifier=LogicalCoreList(self.sut_node.lcores)
+        eal_para = compute_eal_params(
+            self.sut_node, lcore_filter_specifier=LogicalCoreList(self.sut_node.lcores)
         )
         result = self.sut_node.run_dpdk_app(self.app_helloworld_path, eal_para, 50)
         for lcore in self.sut_node.lcores:
diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index 6d206c1a40..43cf5c61eb 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -16,14 +16,13 @@
 """
 
 import struct
-from dataclasses import asdict
 
 from scapy.layers.inet import IP  # type: ignore[import-untyped]
 from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
 from scapy.packet import Raw  # type: ignore[import-untyped]
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
-from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
+from framework.params.testpmd import SimpleForwardingModes
 from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.test_suite import TestSuite
 
@@ -103,17 +102,13 @@ def pmd_scatter(self, mbsize: int) -> None:
         Test:
             Start testpmd and run functional test with preset mbsize.
         """
-        testpmd = self.sut_node.create_interactive_shell(
-            TestPmdShell,
-            app_params=TestPmdParams(
-                forward_mode=SimpleForwardingModes.mac,
-                mbcache=200,
-                mbuf_size=[mbsize],
-                max_pkt_len=9000,
-                tx_offloads=0x00008000,
-                **asdict(self.sut_node.create_eal_parameters()),
-            ),
-            privileged=True,
+        testpmd = TestPmdShell(
+            self.sut_node,
+            forward_mode=SimpleForwardingModes.mac,
+            mbcache=200,
+            mbuf_size=[mbsize],
+            max_pkt_len=9000,
+            tx_offloads=0x00008000,
         )
         testpmd.start()
 
diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py
index ca678f662d..eca27acfd8 100644
--- a/dts/tests/TestSuite_smoke_tests.py
+++ b/dts/tests/TestSuite_smoke_tests.py
@@ -99,7 +99,7 @@ def test_devices_listed_in_testpmd(self) -> None:
         Test:
             List all devices found in testpmd and verify the configured devices are among them.
         """
-        testpmd_driver = self.sut_node.create_interactive_shell(TestPmdShell, privileged=True)
+        testpmd_driver = TestPmdShell(self.sut_node)
         dev_list = [str(x) for x in testpmd_driver.get_devices()]
         for nic in self.nics_in_node:
             self.verify(
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v3 8/8] dts: use Unpack for type checking and hinting
  2024-05-30 15:24 ` [PATCH v3 " Luca Vizzarro
                     ` (6 preceding siblings ...)
  2024-05-30 15:25   ` [PATCH v3 7/8] dts: rework interactive shells Luca Vizzarro
@ 2024-05-30 15:25   ` Luca Vizzarro
  2024-05-30 20:13     ` Jeremy Spewock
                       ` (2 more replies)
  7 siblings, 3 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-05-30 15:25 UTC (permalink / raw)
  To: dev; +Cc: Juraj Linkeš, Jeremy Spewock, Luca Vizzarro, Paul Szczepanek

Interactive shells that inherit DPDKShell initialise their params
classes from a kwargs dict. Therefore, static type checking is
disabled. This change uses the functionality of Unpack added in
PEP 692 to re-enable it. The disadvantage is that this functionality has
been implemented only with TypedDict, forcing the creation of TypedDict
mirrors of the Params classes.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 dts/framework/params/types.py                 | 133 ++++++++++++++++++
 dts/framework/remote_session/testpmd_shell.py |   5 +-
 2 files changed, 137 insertions(+), 1 deletion(-)
 create mode 100644 dts/framework/params/types.py

diff --git a/dts/framework/params/types.py b/dts/framework/params/types.py
new file mode 100644
index 0000000000..e668f658d8
--- /dev/null
+++ b/dts/framework/params/types.py
@@ -0,0 +1,133 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Module containing TypeDict-equivalents of Params classes for static typing and hinting.
+
+TypedDicts can be used in conjunction with Unpack and kwargs for type hinting on function calls.
+
+Example:
+    ..code:: python
+        def create_testpmd(**kwargs: Unpack[TestPmdParamsDict]):
+            params = TestPmdParams(**kwargs)
+"""
+
+from pathlib import PurePath
+from typing import TypedDict
+
+from framework.params import Switch, YesNoSwitch
+from framework.params.testpmd import (
+    AnonMempoolAllocationMode,
+    EthPeer,
+    Event,
+    FlowGenForwardingMode,
+    HairpinMode,
+    NoisyForwardingMode,
+    Params,
+    PortNUMAConfig,
+    PortTopology,
+    RingNUMAConfig,
+    RSSSetting,
+    RXMultiQueueMode,
+    RXRingParams,
+    SimpleForwardingModes,
+    SimpleMempoolAllocationMode,
+    TxIPAddrPair,
+    TXOnlyForwardingMode,
+    TXRingParams,
+    TxUDPPortPair,
+)
+from framework.testbed_model.cpu import LogicalCoreList
+from framework.testbed_model.port import Port
+from framework.testbed_model.virtual_device import VirtualDevice
+
+
+class EalParamsDict(TypedDict, total=False):
+    """:class:`TypedDict` equivalent of :class:`~.eal.EalParams`."""
+
+    lcore_list: LogicalCoreList | None
+    memory_channels: int | None
+    prefix: str
+    no_pci: Switch
+    vdevs: list[VirtualDevice] | None
+    ports: list[Port] | None
+    other_eal_param: Params | None
+
+
+class TestPmdParamsDict(EalParamsDict, total=False):
+    """:class:`TypedDict` equivalent of :class:`~.testpmd.TestPmdParams`."""
+
+    interactive_mode: Switch
+    auto_start: Switch
+    tx_first: Switch
+    stats_period: int | None
+    display_xstats: list[str] | None
+    nb_cores: int | None
+    coremask: int | None
+    nb_ports: int | None
+    port_topology: PortTopology | None
+    portmask: int | None
+    portlist: str | None
+    numa: YesNoSwitch
+    socket_num: int | None
+    port_numa_config: list[PortNUMAConfig] | None
+    ring_numa_config: list[RingNUMAConfig] | None
+    total_num_mbufs: int | None
+    mbuf_size: list[int] | None
+    mbcache: int | None
+    max_pkt_len: int | None
+    eth_peers_configfile: PurePath | None
+    eth_peer: list[EthPeer] | None
+    tx_ip: TxIPAddrPair | None
+    tx_udp: TxUDPPortPair | None
+    enable_lro: Switch
+    max_lro_pkt_size: int | None
+    disable_crc_strip: Switch
+    enable_scatter: Switch
+    enable_hw_vlan: Switch
+    enable_hw_vlan_filter: Switch
+    enable_hw_vlan_strip: Switch
+    enable_hw_vlan_extend: Switch
+    enable_hw_qinq_strip: Switch
+    pkt_drop_enabled: Switch
+    rss: RSSSetting | None
+    forward_mode: (
+        SimpleForwardingModes
+        | FlowGenForwardingMode
+        | TXOnlyForwardingMode
+        | NoisyForwardingMode
+        | None
+    )
+    hairpin_mode: HairpinMode | None
+    hairpin_queues: int | None
+    burst: int | None
+    enable_rx_cksum: Switch
+    rx_queues: int | None
+    rx_ring: RXRingParams | None
+    no_flush_rx: Switch
+    rx_segments_offsets: list[int] | None
+    rx_segments_length: list[int] | None
+    multi_rx_mempool: Switch
+    rx_shared_queue: Switch | int
+    rx_offloads: int | None
+    rx_mq_mode: RXMultiQueueMode | None
+    tx_queues: int | None
+    tx_ring: TXRingParams | None
+    tx_offloads: int | None
+    eth_link_speed: int | None
+    disable_link_check: Switch
+    disable_device_start: Switch
+    no_lsc_interrupt: Switch
+    no_rmv_interrupt: Switch
+    bitrate_stats: int | None
+    latencystats: int | None
+    print_events: list[Event] | None
+    mask_events: list[Event] | None
+    flow_isolate_all: Switch
+    disable_flow_flush: Switch
+    hot_plug: Switch
+    vxlan_gpe_port: int | None
+    geneve_parsed_port: int | None
+    lock_all_memory: YesNoSwitch
+    mempool_allocation_mode: SimpleMempoolAllocationMode | AnonMempoolAllocationMode | None
+    record_core_cycles: Switch
+    record_burst_status: Switch
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index 39985000b9..4114f946a8 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -18,8 +18,11 @@
 from pathlib import PurePath
 from typing import ClassVar
 
+from typing_extensions import Unpack
+
 from framework.exception import InteractiveCommandExecutionError
 from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
+from framework.params.types import TestPmdParamsDict
 from framework.remote_session.dpdk_shell import DPDKShell
 from framework.settings import SETTINGS
 from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
@@ -76,7 +79,7 @@ def __init__(
         ascending_cores: bool = True,
         append_prefix_timestamp: bool = True,
         start_on_init: bool = True,
-        **app_params,
+        **app_params: Unpack[TestPmdParamsDict],
     ) -> None:
         """Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs."""
         super().__init__(
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 1/8] dts: add params manipulation module
  2024-05-30 15:24   ` [PATCH v3 1/8] dts: add params manipulation module Luca Vizzarro
@ 2024-05-30 20:12     ` Jeremy Spewock
  2024-05-31 15:19     ` Nicholas Pratte
  1 sibling, 0 replies; 159+ messages in thread
From: Jeremy Spewock @ 2024-05-30 20:12 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Paul Szczepanek

Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 2/8] dts: use Params for interactive shells
  2024-05-30 15:24   ` [PATCH v3 2/8] dts: use Params for interactive shells Luca Vizzarro
@ 2024-05-30 20:12     ` Jeremy Spewock
  2024-05-31 15:20     ` Nicholas Pratte
  1 sibling, 0 replies; 159+ messages in thread
From: Jeremy Spewock @ 2024-05-30 20:12 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Paul Szczepanek

Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 3/8] dts: refactor EalParams
  2024-05-30 15:25   ` [PATCH v3 3/8] dts: refactor EalParams Luca Vizzarro
@ 2024-05-30 20:12     ` Jeremy Spewock
  2024-05-31 15:21     ` Nicholas Pratte
  1 sibling, 0 replies; 159+ messages in thread
From: Jeremy Spewock @ 2024-05-30 20:12 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Paul Szczepanek

Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 4/8] dts: remove module-wide imports
  2024-05-30 15:25   ` [PATCH v3 4/8] dts: remove module-wide imports Luca Vizzarro
@ 2024-05-30 20:12     ` Jeremy Spewock
  2024-05-31 15:21     ` Nicholas Pratte
  1 sibling, 0 replies; 159+ messages in thread
From: Jeremy Spewock @ 2024-05-30 20:12 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Paul Szczepanek

Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 5/8] dts: add testpmd shell params
  2024-05-30 15:25   ` [PATCH v3 5/8] dts: add testpmd shell params Luca Vizzarro
@ 2024-05-30 20:12     ` Jeremy Spewock
  2024-05-31 15:20     ` Nicholas Pratte
  2024-06-06 14:37     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Jeremy Spewock @ 2024-05-30 20:12 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Paul Szczepanek

Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 6/8] dts: use testpmd params for scatter test suite
  2024-05-30 15:25   ` [PATCH v3 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
@ 2024-05-30 20:13     ` Jeremy Spewock
  2024-05-31 15:22     ` Nicholas Pratte
  2024-06-06 14:38     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Jeremy Spewock @ 2024-05-30 20:13 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Paul Szczepanek

Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 7/8] dts: rework interactive shells
  2024-05-30 15:25   ` [PATCH v3 7/8] dts: rework interactive shells Luca Vizzarro
@ 2024-05-30 20:13     ` Jeremy Spewock
  2024-05-31 15:22     ` Nicholas Pratte
  2024-06-06 18:03     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Jeremy Spewock @ 2024-05-30 20:13 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Paul Szczepanek

Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 8/8] dts: use Unpack for type checking and hinting
  2024-05-30 15:25   ` [PATCH v3 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
@ 2024-05-30 20:13     ` Jeremy Spewock
  2024-05-31 15:21     ` Nicholas Pratte
  2024-06-06 18:05     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Jeremy Spewock @ 2024-05-30 20:13 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Paul Szczepanek

Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 1/8] dts: add params manipulation module
  2024-05-30 15:24   ` [PATCH v3 1/8] dts: add params manipulation module Luca Vizzarro
  2024-05-30 20:12     ` Jeremy Spewock
@ 2024-05-31 15:19     ` Nicholas Pratte
  1 sibling, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-05-31 15:19 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Jeremy Spewock, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Thu, May 30, 2024 at 11:25 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> This commit introduces a new "params" module, which adds a new way
> to manage command line parameters. The provided Params dataclass
> is able to read the fields of its child class and produce a string
> representation to supply to the command line. Any data structure
> that is intended to represent command line parameters can inherit it.
>
> The main purpose is to make it easier to represent data structures that
> map to parameters. Aiding quicker development, while minimising code
> bloat.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>  dts/framework/params/__init__.py | 274 +++++++++++++++++++++++++++++++
>  1 file changed, 274 insertions(+)
>  create mode 100644 dts/framework/params/__init__.py
>
> diff --git a/dts/framework/params/__init__.py b/dts/framework/params/__init__.py
> new file mode 100644
> index 0000000000..18fedcf1ff
> --- /dev/null
> +++ b/dts/framework/params/__init__.py
> @@ -0,0 +1,274 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2024 Arm Limited
> +
> +"""Parameter manipulation module.
> +
> +This module provides :class:`Params` which can be used to model any data structure
> +that is meant to represent any command parameters.
> +"""
> +
> +from dataclasses import dataclass, fields
> +from enum import Flag
> +from typing import Any, Callable, Iterable, Literal, Reversible, TypedDict, cast
> +
> +from typing_extensions import Self
> +
> +#: Type for a function taking one argument.
> +FnPtr = Callable[[Any], Any]
> +#: Type for a switch parameter.
> +Switch = Literal[True, None]
> +#: Type for a yes/no switch parameter.
> +YesNoSwitch = Literal[True, False, None]
> +
> +
> +def _reduce_functions(funcs: Reversible[FnPtr]) -> FnPtr:
> +    """Reduces an iterable of :attr:`FnPtr` from end to start to a composite function.
> +
> +    If the iterable is empty, the created function just returns its fed value back.
> +    """
> +
> +    def composite_function(value: Any):
> +        for fn in reversed(funcs):
> +            value = fn(value)
> +        return value
> +
> +    return composite_function
> +
> +
> +def convert_str(*funcs: FnPtr):
> +    """Decorator that makes the ``__str__`` method a composite function created from its arguments.
> +
> +    The :attr:`FnPtr`s fed to the decorator are executed from right to left
> +    in the arguments list order.
> +
> +    Example:
> +    .. code:: python
> +
> +        @convert_str(hex_from_flag_value)
> +        class BitMask(enum.Flag):
> +            A = auto()
> +            B = auto()
> +
> +    will allow ``BitMask`` to render as a hexadecimal value.
> +    """
> +
> +    def _class_decorator(original_class):
> +        original_class.__str__ = _reduce_functions(funcs)
> +        return original_class
> +
> +    return _class_decorator
> +
> +
> +def comma_separated(values: Iterable[Any]) -> str:
> +    """Converts an iterable into a comma-separated string."""
> +    return ",".join([str(value).strip() for value in values if value is not None])
> +
> +
> +def bracketed(value: str) -> str:
> +    """Adds round brackets to the input."""
> +    return f"({value})"
> +
> +
> +def str_from_flag_value(flag: Flag) -> str:
> +    """Returns the value from a :class:`enum.Flag` as a string."""
> +    return str(flag.value)
> +
> +
> +def hex_from_flag_value(flag: Flag) -> str:
> +    """Returns the value from a :class:`enum.Flag` converted to hexadecimal."""
> +    return hex(flag.value)
> +
> +
> +class ParamsModifier(TypedDict, total=False):
> +    """Params modifiers dict compatible with the :func:`dataclasses.field` metadata parameter."""
> +
> +    #:
> +    Params_value_only: bool
> +    #:
> +    Params_short: str
> +    #:
> +    Params_long: str
> +    #:
> +    Params_multiple: bool
> +    #:
> +    Params_convert_value: Reversible[FnPtr]
> +
> +
> +@dataclass
> +class Params:
> +    """Dataclass that renders its fields into command line arguments.
> +
> +    The parameter name is taken from the field name by default. The following:
> +
> +    .. code:: python
> +
> +        name: str | None = "value"
> +
> +    is rendered as ``--name=value``.
> +    Through :func:`dataclasses.field` the resulting parameter can be manipulated by applying
> +    this class' metadata modifier functions.
> +
> +    To use fields as switches, set the value to ``True`` to render them. If you
> +    use a yes/no switch you can also set ``False`` which would render a switch
> +    prefixed with ``--no-``. Examples:
> +
> +    .. code:: python
> +
> +        interactive: Switch = True  # renders --interactive
> +        numa: YesNoSwitch   = False # renders --no-numa
> +
> +    Setting ``None`` will prevent it from being rendered. The :attr:`~Switch` type alias is provided
> +    for regular switches, whereas :attr:`~YesNoSwitch` is offered for yes/no ones.
> +
> +    An instance of a dataclass inheriting ``Params`` can also be assigned to an attribute,
> +    this helps with grouping parameters together.
> +    The attribute holding the dataclass will be ignored and the latter will just be rendered as
> +    expected.
> +    """
> +
> +    _suffix = ""
> +    """Holder of the plain text value of Params when called directly. A suffix for child classes."""
> +
> +    """========= BEGIN FIELD METADATA MODIFIER FUNCTIONS ========"""
> +
> +    @staticmethod
> +    def value_only() -> ParamsModifier:
> +        """Injects the value of the attribute as-is without flag.
> +
> +        Metadata modifier for :func:`dataclasses.field`.
> +        """
> +        return ParamsModifier(Params_value_only=True)
> +
> +    @staticmethod
> +    def short(name: str) -> ParamsModifier:
> +        """Overrides any parameter name with the given short option.
> +
> +        Metadata modifier for :func:`dataclasses.field`.
> +
> +        Example:
> +        .. code:: python
> +
> +            logical_cores: str | None = field(default="1-4", metadata=Params.short("l"))
> +
> +        will render as ``-l=1-4`` instead of ``--logical-cores=1-4``.
> +        """
> +        return ParamsModifier(Params_short=name)
> +
> +    @staticmethod
> +    def long(name: str) -> ParamsModifier:
> +        """Overrides the inferred parameter name to the specified one.
> +
> +        Metadata modifier for :func:`dataclasses.field`.
> +
> +        Example:
> +        .. code:: python
> +
> +            x_name: str | None = field(default="y", metadata=Params.long("x"))
> +
> +        will render as ``--x=y``, but the field is accessed and modified through ``x_name``.
> +        """
> +        return ParamsModifier(Params_long=name)
> +
> +    @staticmethod
> +    def multiple() -> ParamsModifier:
> +        """Specifies that this parameter is set multiple times. Must be a list.
> +
> +        Metadata modifier for :func:`dataclasses.field`.
> +
> +        Example:
> +        .. code:: python
> +
> +            ports: list[int] | None = field(
> +                default_factory=lambda: [0, 1, 2],
> +                metadata=Params.multiple() | Params.long("port")
> +            )
> +
> +        will render as ``--port=0 --port=1 --port=2``. Note that modifiers can be chained like
> +        in this example.
> +        """
> +        return ParamsModifier(Params_multiple=True)
> +
> +    @classmethod
> +    def convert_value(cls, *funcs: FnPtr) -> ParamsModifier:
> +        """Takes in a variable number of functions to convert the value text representation.
> +
> +        Metadata modifier for :func:`dataclasses.field`.
> +
> +        The ``metadata`` keyword argument can be used to chain metadata modifiers together.
> +
> +        Functions can be chained together, executed from right to left in the arguments list order.
> +
> +        Example:
> +        .. code:: python
> +
> +            hex_bitmask: int | None = field(
> +                default=0b1101,
> +                metadata=Params.convert_value(hex) | Params.long("mask")
> +            )
> +
> +        will render as ``--mask=0xd``.
> +        """
> +        return ParamsModifier(Params_convert_value=funcs)
> +
> +    """========= END FIELD METADATA MODIFIER FUNCTIONS ========"""
> +
> +    def append_str(self, text: str) -> None:
> +        """Appends a string at the end of the string representation."""
> +        self._suffix += text
> +
> +    def __iadd__(self, text: str) -> Self:
> +        """Appends a string at the end of the string representation."""
> +        self.append_str(text)
> +        return self
> +
> +    @classmethod
> +    def from_str(cls, text: str) -> Self:
> +        """Creates a plain Params object from a string."""
> +        obj = cls()
> +        obj.append_str(text)
> +        return obj
> +
> +    @staticmethod
> +    def _make_switch(
> +        name: str, is_short: bool = False, is_no: bool = False, value: str | None = None
> +    ) -> str:
> +        prefix = f"{'-' if is_short else '--'}{'no-' if is_no else ''}"
> +        name = name.replace("_", "-")
> +        value = f"{' ' if is_short else '='}{value}" if value else ""
> +        return f"{prefix}{name}{value}"
> +
> +    def __str__(self) -> str:
> +        """Returns a string of command-line-ready arguments from the class fields."""
> +        arguments: list[str] = []
> +
> +        for field in fields(self):
> +            value = getattr(self, field.name)
> +            modifiers = cast(ParamsModifier, field.metadata)
> +
> +            if value is None:
> +                continue
> +
> +            value_only = modifiers.get("Params_value_only", False)
> +            if isinstance(value, Params) or value_only:
> +                arguments.append(str(value))
> +                continue
> +
> +            # take the short modifier, or the long modifier, or infer from field name
> +            switch_name = modifiers.get("Params_short", modifiers.get("Params_long", field.name))
> +            is_short = "Params_short" in modifiers
> +
> +            if isinstance(value, bool):
> +                arguments.append(self._make_switch(switch_name, is_short, is_no=(not value)))
> +                continue
> +
> +            convert = _reduce_functions(modifiers.get("Params_convert_value", []))
> +            multiple = modifiers.get("Params_multiple", False)
> +
> +            values = value if multiple else [value]
> +            for value in values:
> +                arguments.append(self._make_switch(switch_name, is_short, value=convert(value)))
> +
> +        if self._suffix:
> +            arguments.append(self._suffix)
> +
> +        return " ".join(arguments)
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 2/8] dts: use Params for interactive shells
  2024-05-30 15:24   ` [PATCH v3 2/8] dts: use Params for interactive shells Luca Vizzarro
  2024-05-30 20:12     ` Jeremy Spewock
@ 2024-05-31 15:20     ` Nicholas Pratte
  1 sibling, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-05-31 15:20 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Jeremy Spewock, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Thu, May 30, 2024 at 11:25 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Make it so that interactive shells accept an implementation of `Params`
> for app arguments. Convert EalParameters to use `Params` instead.
>
> String command line parameters can still be supplied by using the
> `Params.from_str()` method.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>  .../remote_session/interactive_shell.py       |  12 +-
>  dts/framework/remote_session/testpmd_shell.py |  11 +-
>  dts/framework/testbed_model/node.py           |   6 +-
>  dts/framework/testbed_model/os_session.py     |   4 +-
>  dts/framework/testbed_model/sut_node.py       | 124 ++++++++----------
>  dts/tests/TestSuite_pmd_buffer_scatter.py     |   3 +-
>  6 files changed, 77 insertions(+), 83 deletions(-)
>
> diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
> index 074a541279..9da66d1c7e 100644
> --- a/dts/framework/remote_session/interactive_shell.py
> +++ b/dts/framework/remote_session/interactive_shell.py
> @@ -1,5 +1,6 @@
>  # SPDX-License-Identifier: BSD-3-Clause
>  # Copyright(c) 2023 University of New Hampshire
> +# Copyright(c) 2024 Arm Limited
>
>  """Common functionality for interactive shell handling.
>
> @@ -21,6 +22,7 @@
>  from paramiko import Channel, SSHClient, channel  # type: ignore[import-untyped]
>
>  from framework.logger import DTSLogger
> +from framework.params import Params
>  from framework.settings import SETTINGS
>
>
> @@ -40,7 +42,7 @@ class InteractiveShell(ABC):
>      _ssh_channel: Channel
>      _logger: DTSLogger
>      _timeout: float
> -    _app_args: str
> +    _app_params: Params
>
>      #: Prompt to expect at the end of output when sending a command.
>      #: This is often overridden by subclasses.
> @@ -63,7 +65,7 @@ def __init__(
>          interactive_session: SSHClient,
>          logger: DTSLogger,
>          get_privileged_command: Callable[[str], str] | None,
> -        app_args: str = "",
> +        app_params: Params = Params(),
>          timeout: float = SETTINGS.timeout,
>      ) -> None:
>          """Create an SSH channel during initialization.
> @@ -74,7 +76,7 @@ def __init__(
>              get_privileged_command: A method for modifying a command to allow it to use
>                  elevated privileges. If :data:`None`, the application will not be started
>                  with elevated privileges.
> -            app_args: The command line arguments to be passed to the application on startup.
> +            app_params: The command line parameters to be passed to the application on startup.
>              timeout: The timeout used for the SSH channel that is dedicated to this interactive
>                  shell. This timeout is for collecting output, so if reading from the buffer
>                  and no output is gathered within the timeout, an exception is thrown.
> @@ -87,7 +89,7 @@ def __init__(
>          self._ssh_channel.set_combine_stderr(True)  # combines stdout and stderr streams
>          self._logger = logger
>          self._timeout = timeout
> -        self._app_args = app_args
> +        self._app_params = app_params
>          self._start_application(get_privileged_command)
>
>      def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
> @@ -100,7 +102,7 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
>              get_privileged_command: A function (but could be any callable) that produces
>                  the version of the command with elevated privileges.
>          """
> -        start_command = f"{self.path} {self._app_args}"
> +        start_command = f"{self.path} {self._app_params}"
>          if get_privileged_command is not None:
>              start_command = get_privileged_command(start_command)
>          self.send_command(start_command)
> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
> index cb2ab6bd00..7eced27096 100644
> --- a/dts/framework/remote_session/testpmd_shell.py
> +++ b/dts/framework/remote_session/testpmd_shell.py
> @@ -22,6 +22,7 @@
>
>  from framework.exception import InteractiveCommandExecutionError
>  from framework.settings import SETTINGS
> +from framework.testbed_model.sut_node import EalParams
>  from framework.utils import StrEnum
>
>  from .interactive_shell import InteractiveShell
> @@ -118,8 +119,14 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
>          Also find the number of pci addresses which were allowed on the command line when the app
>          was started.
>          """
> -        self._app_args += " -i --mask-event intr_lsc"
> -        self.number_of_ports = self._app_args.count("-a ")
> +        self._app_params += " -i --mask-event intr_lsc"
> +
> +        assert isinstance(self._app_params, EalParams)
> +
> +        self.number_of_ports = (
> +            len(self._app_params.ports) if self._app_params.ports is not None else 0
> +        )
> +
>          super()._start_application(get_privileged_command)
>
>      def start(self, verify: bool = True) -> None:
> diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
> index 74061f6262..6af4f25a3c 100644
> --- a/dts/framework/testbed_model/node.py
> +++ b/dts/framework/testbed_model/node.py
> @@ -2,6 +2,7 @@
>  # Copyright(c) 2010-2014 Intel Corporation
>  # Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
>  # Copyright(c) 2022-2023 University of New Hampshire
> +# Copyright(c) 2024 Arm Limited
>
>  """Common functionality for node management.
>
> @@ -24,6 +25,7 @@
>  )
>  from framework.exception import ConfigurationError
>  from framework.logger import DTSLogger, get_dts_logger
> +from framework.params import Params
>  from framework.settings import SETTINGS
>
>  from .cpu import (
> @@ -199,7 +201,7 @@ def create_interactive_shell(
>          shell_cls: Type[InteractiveShellType],
>          timeout: float = SETTINGS.timeout,
>          privileged: bool = False,
> -        app_args: str = "",
> +        app_params: Params = Params(),
>      ) -> InteractiveShellType:
>          """Factory for interactive session handlers.
>
> @@ -222,7 +224,7 @@ def create_interactive_shell(
>              shell_cls,
>              timeout,
>              privileged,
> -            app_args,
> +            app_params,
>          )
>
>      def filter_lcores(
> diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
> index d5bf7e0401..1a77aee532 100644
> --- a/dts/framework/testbed_model/os_session.py
> +++ b/dts/framework/testbed_model/os_session.py
> @@ -1,6 +1,7 @@
>  # SPDX-License-Identifier: BSD-3-Clause
>  # Copyright(c) 2023 PANTHEON.tech s.r.o.
>  # Copyright(c) 2023 University of New Hampshire
> +# Copyright(c) 2024 Arm Limited
>
>  """OS-aware remote session.
>
> @@ -29,6 +30,7 @@
>
>  from framework.config import Architecture, NodeConfiguration, NodeInfo
>  from framework.logger import DTSLogger
> +from framework.params import Params
>  from framework.remote_session import (
>      CommandResult,
>      InteractiveRemoteSession,
> @@ -134,7 +136,7 @@ def create_interactive_shell(
>          shell_cls: Type[InteractiveShellType],
>          timeout: float,
>          privileged: bool,
> -        app_args: str,
> +        app_args: Params,
>      ) -> InteractiveShellType:
>          """Factory for interactive session handlers.
>
> diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
> index 97aa26d419..c886590979 100644
> --- a/dts/framework/testbed_model/sut_node.py
> +++ b/dts/framework/testbed_model/sut_node.py
> @@ -2,6 +2,7 @@
>  # Copyright(c) 2010-2014 Intel Corporation
>  # Copyright(c) 2023 PANTHEON.tech s.r.o.
>  # Copyright(c) 2023 University of New Hampshire
> +# Copyright(c) 2024 Arm Limited
>
>  """System under test (DPDK + hardware) node.
>
> @@ -14,8 +15,9 @@
>  import os
>  import tarfile
>  import time
> +from dataclasses import dataclass, field
>  from pathlib import PurePath
> -from typing import Type
> +from typing import Literal, Type
>
>  from framework.config import (
>      BuildTargetConfiguration,
> @@ -23,6 +25,7 @@
>      NodeInfo,
>      SutNodeConfiguration,
>  )
> +from framework.params import Params, Switch
>  from framework.remote_session import CommandResult
>  from framework.settings import SETTINGS
>  from framework.utils import MesonArgs
> @@ -34,62 +37,42 @@
>  from .virtual_device import VirtualDevice
>
>
> -class EalParameters(object):
> -    """The environment abstraction layer parameters.
> -
> -    The string representation can be created by converting the instance to a string.
> -    """
> +def _port_to_pci(port: Port) -> str:
> +    return port.pci
>
> -    def __init__(
> -        self,
> -        lcore_list: LogicalCoreList,
> -        memory_channels: int,
> -        prefix: str,
> -        no_pci: bool,
> -        vdevs: list[VirtualDevice],
> -        ports: list[Port],
> -        other_eal_param: str,
> -    ):
> -        """Initialize the parameters according to inputs.
> -
> -        Process the parameters into the format used on the command line.
>
> -        Args:
> -            lcore_list: The list of logical cores to use.
> -            memory_channels: The number of memory channels to use.
> -            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
> -            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
> -            vdevs: Virtual devices, e.g.::
> +@dataclass(kw_only=True)
> +class EalParams(Params):
> +    """The environment abstraction layer parameters.
>
> -                vdevs=[
> -                    VirtualDevice('net_ring0'),
> -                    VirtualDevice('net_ring1')
> -                ]
> -            ports: The list of ports to allow.
> -            other_eal_param: user defined DPDK EAL parameters, e.g.:
> +    Attributes:
> +        lcore_list: The list of logical cores to use.
> +        memory_channels: The number of memory channels to use.
> +        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
> +        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
> +        vdevs: Virtual devices, e.g.::
> +            vdevs=[
> +                VirtualDevice('net_ring0'),
> +                VirtualDevice('net_ring1')
> +            ]
> +        ports: The list of ports to allow.
> +        other_eal_param: user defined DPDK EAL parameters, e.g.:
>                  ``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._ports = " ".join(f"-a {port.pci}" for port in ports)
> -        self._other_eal_param = other_eal_param
> -
> -    def __str__(self) -> str:
> -        """Create the EAL string."""
> -        return (
> -            f"{self._lcore_list} "
> -            f"{self._memory_channels} "
> -            f"{self._prefix} "
> -            f"{self._no_pci} "
> -            f"{self._vdevs} "
> -            f"{self._ports} "
> -            f"{self._other_eal_param}"
> -        )
> +    """
> +
> +    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
> +    memory_channels: int = field(metadata=Params.short("n"))
> +    prefix: str = field(metadata=Params.long("file-prefix"))
> +    no_pci: Switch
> +    vdevs: list[VirtualDevice] | None = field(
> +        default=None, metadata=Params.multiple() | Params.long("vdev")
> +    )
> +    ports: list[Port] | None = field(
> +        default=None,
> +        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
> +    )
> +    other_eal_param: Params | None = None
> +    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
>
>
>  class SutNode(Node):
> @@ -350,11 +333,11 @@ def create_eal_parameters(
>          ascending_cores: bool = True,
>          prefix: str = "dpdk",
>          append_prefix_timestamp: bool = True,
> -        no_pci: bool = False,
> +        no_pci: Switch = None,
>          vdevs: list[VirtualDevice] | None = None,
>          ports: list[Port] | None = None,
>          other_eal_param: str = "",
> -    ) -> "EalParameters":
> +    ) -> EalParams:
>          """Compose the EAL parameters.
>
>          Process the list of cores and the DPDK prefix and pass that along with
> @@ -393,24 +376,21 @@ def create_eal_parameters(
>          if prefix:
>              self._dpdk_prefix_list.append(prefix)
>
> -        if vdevs is None:
> -            vdevs = []
> -
>          if ports is None:
>              ports = self.ports
>
> -        return EalParameters(
> +        return EalParams(
>              lcore_list=lcore_list,
>              memory_channels=self.config.memory_channels,
>              prefix=prefix,
>              no_pci=no_pci,
>              vdevs=vdevs,
>              ports=ports,
> -            other_eal_param=other_eal_param,
> +            other_eal_param=Params.from_str(other_eal_param),
>          )
>
>      def run_dpdk_app(
> -        self, app_path: PurePath, eal_args: "EalParameters", timeout: float = 30
> +        self, app_path: PurePath, eal_params: EalParams, timeout: float = 30
>      ) -> CommandResult:
>          """Run DPDK application on the remote node.
>
> @@ -419,14 +399,14 @@ def run_dpdk_app(
>
>          Args:
>              app_path: The remote path to the DPDK application.
> -            eal_args: EAL parameters to run the DPDK application with.
> +            eal_params: EAL parameters to run the DPDK application with.
>              timeout: Wait at most this long in seconds for `command` execution to complete.
>
>          Returns:
>              The result of the DPDK app execution.
>          """
>          return self.main_session.send_command(
> -            f"{app_path} {eal_args}", timeout, privileged=True, verify=True
> +            f"{app_path} {eal_params}", timeout, privileged=True, verify=True
>          )
>
>      def configure_ipv4_forwarding(self, enable: bool) -> None:
> @@ -442,8 +422,8 @@ def create_interactive_shell(
>          shell_cls: Type[InteractiveShellType],
>          timeout: float = SETTINGS.timeout,
>          privileged: bool = False,
> -        app_parameters: str = "",
> -        eal_parameters: EalParameters | None = None,
> +        app_params: Params = Params(),
> +        eal_params: EalParams | None = None,
>      ) -> InteractiveShellType:
>          """Extend the factory for interactive session handlers.
>
> @@ -459,26 +439,26 @@ def create_interactive_shell(
>                  reading from the buffer and don't receive any data within the timeout
>                  it will throw an error.
>              privileged: Whether to run the shell with administrative privileges.
> -            eal_parameters: List of EAL parameters to use to launch the app. If this
> +            app_params: The parameters to be passed to the application.
> +            eal_params: List of EAL parameters to use to launch the app. If this
>                  isn't provided or an empty string is passed, it will default to calling
>                  :meth:`create_eal_parameters`.
> -            app_parameters: Additional arguments to pass into the application on the
> -                command-line.
>
>          Returns:
>              An instance of the desired interactive application shell.
>          """
>          # We need to append the build directory and add EAL parameters for DPDK apps
>          if shell_cls.dpdk_app:
> -            if not eal_parameters:
> -                eal_parameters = self.create_eal_parameters()
> -            app_parameters = f"{eal_parameters} -- {app_parameters}"
> +            if eal_params is None:
> +                eal_params = self.create_eal_parameters()
> +            eal_params.append_str(str(app_params))
> +            app_params = eal_params
>
>              shell_cls.path = self.main_session.join_remote_path(
>                  self.remote_dpdk_build_dir, shell_cls.path
>              )
>
> -        return super().create_interactive_shell(shell_cls, timeout, privileged, app_parameters)
> +        return super().create_interactive_shell(shell_cls, timeout, privileged, app_params)
>
>      def bind_ports_to_driver(self, for_dpdk: bool = True) -> None:
>          """Bind all ports on the SUT to a driver.
> diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
> index a020682e8d..c6e93839cb 100644
> --- a/dts/tests/TestSuite_pmd_buffer_scatter.py
> +++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
> @@ -22,6 +22,7 @@
>  from scapy.packet import Raw  # type: ignore[import-untyped]
>  from scapy.utils import hexstr  # type: ignore[import-untyped]
>
> +from framework.params import Params
>  from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
>  from framework.test_suite import TestSuite
>
> @@ -103,7 +104,7 @@ def pmd_scatter(self, mbsize: int) -> None:
>          """
>          testpmd = self.sut_node.create_interactive_shell(
>              TestPmdShell,
> -            app_parameters=(
> +            app_params=Params.from_str(
>                  "--mbcache=200 "
>                  f"--mbuf-size={mbsize} "
>                  "--max-pkt-len=9000 "
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 5/8] dts: add testpmd shell params
  2024-05-30 15:25   ` [PATCH v3 5/8] dts: add testpmd shell params Luca Vizzarro
  2024-05-30 20:12     ` Jeremy Spewock
@ 2024-05-31 15:20     ` Nicholas Pratte
  2024-06-06 14:37     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-05-31 15:20 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Jeremy Spewock, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Thu, May 30, 2024 at 11:25 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Implement all the testpmd shell parameters into a data structure.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>  dts/framework/params/testpmd.py               | 609 ++++++++++++++++++
>  dts/framework/remote_session/testpmd_shell.py |  42 +-
>  dts/tests/TestSuite_pmd_buffer_scatter.py     |   5 +-
>  3 files changed, 616 insertions(+), 40 deletions(-)
>  create mode 100644 dts/framework/params/testpmd.py
>
> diff --git a/dts/framework/params/testpmd.py b/dts/framework/params/testpmd.py
> new file mode 100644
> index 0000000000..88d208d683
> --- /dev/null
> +++ b/dts/framework/params/testpmd.py
> @@ -0,0 +1,609 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2024 Arm Limited
> +
> +"""Module containing all the TestPmd-related parameter classes."""
> +
> +from dataclasses import dataclass, field
> +from enum import EnumMeta, Flag, auto, unique
> +from pathlib import PurePath
> +from typing import Literal, NamedTuple
> +
> +from framework.params import (
> +    Params,
> +    Switch,
> +    YesNoSwitch,
> +    bracketed,
> +    comma_separated,
> +    convert_str,
> +    hex_from_flag_value,
> +    str_from_flag_value,
> +)
> +from framework.params.eal import EalParams
> +from framework.utils import StrEnum
> +
> +
> +class PortTopology(StrEnum):
> +    """Enum representing the port topology."""
> +
> +    #: In paired mode, the forwarding is between pairs of ports, e.g.: (0,1), (2,3), (4,5).
> +    paired = auto()
> +
> +    #: In chained mode, the forwarding is to the next available port in the port mask, e.g.:
> +    #: (0,1), (1,2), (2,0).
> +    #:
> +    #: The ordering of the ports can be changed using the portlist testpmd runtime function.
> +    chained = auto()
> +
> +    #: In loop mode, ingress traffic is simply transmitted back on the same interface.
> +    loop = auto()
> +
> +
> +@convert_str(bracketed, comma_separated)
> +class PortNUMAConfig(NamedTuple):
> +    """DPDK port to NUMA socket association tuple."""
> +
> +    #:
> +    port: int
> +    #:
> +    socket: int
> +
> +
> +@convert_str(str_from_flag_value)
> +@unique
> +class FlowDirection(Flag):
> +    """Flag indicating the direction of the flow.
> +
> +    A bi-directional flow can be specified with the pipe:
> +
> +    >>> TestPmdFlowDirection.RX | TestPmdFlowDirection.TX
> +    <TestPmdFlowDirection.TX|RX: 3>
> +    """
> +
> +    #:
> +    RX = 1 << 0
> +    #:
> +    TX = 1 << 1
> +
> +
> +@convert_str(bracketed, comma_separated)
> +class RingNUMAConfig(NamedTuple):
> +    """Tuple associating DPDK port, direction of the flow and NUMA socket."""
> +
> +    #:
> +    port: int
> +    #:
> +    direction: FlowDirection
> +    #:
> +    socket: int
> +
> +
> +@convert_str(comma_separated)
> +class EthPeer(NamedTuple):
> +    """Tuple associating a MAC address to the specified DPDK port."""
> +
> +    #:
> +    port_no: int
> +    #:
> +    mac_address: str
> +
> +
> +@convert_str(comma_separated)
> +class TxIPAddrPair(NamedTuple):
> +    """Tuple specifying the source and destination IPs for the packets."""
> +
> +    #:
> +    source_ip: str
> +    #:
> +    dest_ip: str
> +
> +
> +@convert_str(comma_separated)
> +class TxUDPPortPair(NamedTuple):
> +    """Tuple specifying the UDP source and destination ports for the packets.
> +
> +    If leaving ``dest_port`` unspecified, ``source_port`` will be used for
> +    the destination port as well.
> +    """
> +
> +    #:
> +    source_port: int
> +    #:
> +    dest_port: int | None = None
> +
> +
> +@dataclass
> +class DisableRSS(Params):
> +    """Disables RSS (Receive Side Scaling)."""
> +
> +    _disable_rss: Literal[True] = field(
> +        default=True, init=False, metadata=Params.long("disable-rss")
> +    )
> +
> +
> +@dataclass
> +class SetRSSIPOnly(Params):
> +    """Sets RSS (Receive Side Scaling) functions for IPv4/IPv6 only."""
> +
> +    _rss_ip: Literal[True] = field(default=True, init=False, metadata=Params.long("rss-ip"))
> +
> +
> +@dataclass
> +class SetRSSUDP(Params):
> +    """Sets RSS (Receive Side Scaling) functions for IPv4/IPv6 and UDP."""
> +
> +    _rss_udp: Literal[True] = field(default=True, init=False, metadata=Params.long("rss-udp"))
> +
> +
> +class RSSSetting(EnumMeta):
> +    """Enum representing a RSS setting. Each property is a class that needs to be initialised."""
> +
> +    #:
> +    Disabled = DisableRSS
> +    #:
> +    SetIPOnly = SetRSSIPOnly
> +    #:
> +    SetUDP = SetRSSUDP
> +
> +
> +class SimpleForwardingModes(StrEnum):
> +    r"""The supported packet forwarding modes for :class:`~TestPmdShell`\s."""
> +
> +    #:
> +    io = auto()
> +    #:
> +    mac = auto()
> +    #:
> +    macswap = auto()
> +    #:
> +    rxonly = auto()
> +    #:
> +    csum = auto()
> +    #:
> +    icmpecho = auto()
> +    #:
> +    ieee1588 = auto()
> +    #:
> +    fivetswap = "5tswap"
> +    #:
> +    shared_rxq = "shared-rxq"
> +    #:
> +    recycle_mbufs = auto()
> +
> +
> +@dataclass(kw_only=True)
> +class TXOnlyForwardingMode(Params):
> +    """Sets a TX-Only forwarding mode.
> +
> +    Attributes:
> +        multi_flow: Generates multiple flows if set to True.
> +        segments_length: Sets TX segment sizes or total packet length.
> +    """
> +
> +    _forward_mode: Literal["txonly"] = field(
> +        default="txonly", init=False, metadata=Params.long("forward-mode")
> +    )
> +    multi_flow: Switch = field(default=None, metadata=Params.long("txonly-multi-flow"))
> +    segments_length: list[int] | None = field(
> +        default=None, metadata=Params.long("txpkts") | Params.convert_value(comma_separated)
> +    )
> +
> +
> +@dataclass(kw_only=True)
> +class FlowGenForwardingMode(Params):
> +    """Sets a flowgen forwarding mode.
> +
> +    Attributes:
> +        clones: Set the number of each packet clones to be sent. Sending clones reduces host CPU
> +                load on creating packets and may help in testing extreme speeds or maxing out
> +                Tx packet performance. N should be not zero, but less than ‘burst’ parameter.
> +        flows: Set the number of flows to be generated, where 1 <= N <= INT32_MAX.
> +        segments_length: Set TX segment sizes or total packet length.
> +    """
> +
> +    _forward_mode: Literal["flowgen"] = field(
> +        default="flowgen", init=False, metadata=Params.long("forward-mode")
> +    )
> +    clones: int | None = field(default=None, metadata=Params.long("flowgen-clones"))
> +    flows: int | None = field(default=None, metadata=Params.long("flowgen-flows"))
> +    segments_length: list[int] | None = field(
> +        default=None, metadata=Params.long("txpkts") | Params.convert_value(comma_separated)
> +    )
> +
> +
> +@dataclass(kw_only=True)
> +class NoisyForwardingMode(Params):
> +    """Sets a noisy forwarding mode.
> +
> +    Attributes:
> +        forward_mode: Set the noisy VNF forwarding mode.
> +        tx_sw_buffer_size: Set the maximum number of elements of the FIFO queue to be created for
> +                           buffering packets.
> +        tx_sw_buffer_flushtime: Set the time before packets in the FIFO queue are flushed.
> +        lkup_memory: Set the size of the noisy neighbor simulation memory buffer in MB to N.
> +        lkup_num_reads: Set the size of the noisy neighbor simulation memory buffer in MB to N.
> +        lkup_num_writes: Set the number of writes to be done in noisy neighbor simulation
> +                         memory buffer to N.
> +        lkup_num_reads_writes: Set the number of r/w accesses to be done in noisy neighbor
> +                               simulation memory buffer to N.
> +    """
> +
> +    _forward_mode: Literal["noisy"] = field(
> +        default="noisy", init=False, metadata=Params.long("forward-mode")
> +    )
> +    forward_mode: (
> +        Literal[
> +            SimpleForwardingModes.io,
> +            SimpleForwardingModes.mac,
> +            SimpleForwardingModes.macswap,
> +            SimpleForwardingModes.fivetswap,
> +        ]
> +        | None
> +    ) = field(default=SimpleForwardingModes.io, metadata=Params.long("noisy-forward-mode"))
> +    tx_sw_buffer_size: int | None = field(
> +        default=None, metadata=Params.long("noisy-tx-sw-buffer-size")
> +    )
> +    tx_sw_buffer_flushtime: int | None = field(
> +        default=None, metadata=Params.long("noisy-tx-sw-buffer-flushtime")
> +    )
> +    lkup_memory: int | None = field(default=None, metadata=Params.long("noisy-lkup-memory"))
> +    lkup_num_reads: int | None = field(default=None, metadata=Params.long("noisy-lkup-num-reads"))
> +    lkup_num_writes: int | None = field(default=None, metadata=Params.long("noisy-lkup-num-writes"))
> +    lkup_num_reads_writes: int | None = field(
> +        default=None, metadata=Params.long("noisy-lkup-num-reads-writes")
> +    )
> +
> +
> +@convert_str(hex_from_flag_value)
> +@unique
> +class HairpinMode(Flag):
> +    """Flag representing the hairpin mode."""
> +
> +    #: Two hairpin ports loop.
> +    TWO_PORTS_LOOP = 1 << 0
> +    #: Two hairpin ports paired.
> +    TWO_PORTS_PAIRED = 1 << 1
> +    #: Explicit Tx flow rule.
> +    EXPLICIT_TX_FLOW = 1 << 4
> +    #: Force memory settings of hairpin RX queue.
> +    FORCE_RX_QUEUE_MEM_SETTINGS = 1 << 8
> +    #: Force memory settings of hairpin TX queue.
> +    FORCE_TX_QUEUE_MEM_SETTINGS = 1 << 9
> +    #: Hairpin RX queues will use locked device memory.
> +    RX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 12
> +    #: Hairpin RX queues will use RTE memory.
> +    RX_QUEUE_USE_RTE_MEMORY = 1 << 13
> +    #: Hairpin TX queues will use locked device memory.
> +    TX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 16
> +    #: Hairpin TX queues will use RTE memory.
> +    TX_QUEUE_USE_RTE_MEMORY = 1 << 18
> +
> +
> +@dataclass(kw_only=True)
> +class RXRingParams(Params):
> +    """Sets the RX ring parameters.
> +
> +    Attributes:
> +        descriptors: Set the number of descriptors in the RX rings to N, where N > 0.
> +        prefetch_threshold: Set the prefetch threshold register of RX rings to N, where N >= 0.
> +        host_threshold: Set the host threshold register of RX rings to N, where N >= 0.
> +        write_back_threshold: Set the write-back threshold register of RX rings to N, where N >= 0.
> +        free_threshold: Set the free threshold of RX descriptors to N,
> +                        where 0 <= N < value of ``-–rxd``.
> +    """
> +
> +    descriptors: int | None = field(default=None, metadata=Params.long("rxd"))
> +    prefetch_threshold: int | None = field(default=None, metadata=Params.long("rxpt"))
> +    host_threshold: int | None = field(default=None, metadata=Params.long("rxht"))
> +    write_back_threshold: int | None = field(default=None, metadata=Params.long("rxwt"))
> +    free_threshold: int | None = field(default=None, metadata=Params.long("rxfreet"))
> +
> +
> +@convert_str(hex_from_flag_value)
> +@unique
> +class RXMultiQueueMode(Flag):
> +    """Flag representing the RX multi-queue mode."""
> +
> +    #:
> +    RSS = 1 << 0
> +    #:
> +    DCB = 1 << 1
> +    #:
> +    VMDQ = 1 << 2
> +
> +
> +@dataclass(kw_only=True)
> +class TXRingParams(Params):
> +    """Sets the TX ring parameters.
> +
> +    Attributes:
> +        descriptors: Set the number of descriptors in the TX rings to N, where N > 0.
> +        rs_bit_threshold: Set the transmit RS bit threshold of TX rings to N,
> +                          where 0 <= N <= value of ``--txd``.
> +        prefetch_threshold: Set the prefetch threshold register of TX rings to N, where N >= 0.
> +        host_threshold: Set the host threshold register of TX rings to N, where N >= 0.
> +        write_back_threshold: Set the write-back threshold register of TX rings to N, where N >= 0.
> +        free_threshold: Set the transmit free threshold of TX rings to N,
> +                        where 0 <= N <= value of ``--txd``.
> +    """
> +
> +    descriptors: int | None = field(default=None, metadata=Params.long("txd"))
> +    rs_bit_threshold: int | None = field(default=None, metadata=Params.long("txrst"))
> +    prefetch_threshold: int | None = field(default=None, metadata=Params.long("txpt"))
> +    host_threshold: int | None = field(default=None, metadata=Params.long("txht"))
> +    write_back_threshold: int | None = field(default=None, metadata=Params.long("txwt"))
> +    free_threshold: int | None = field(default=None, metadata=Params.long("txfreet"))
> +
> +
> +class Event(StrEnum):
> +    """Enum representing a testpmd event."""
> +
> +    #:
> +    unknown = auto()
> +    #:
> +    queue_state = auto()
> +    #:
> +    vf_mbox = auto()
> +    #:
> +    macsec = auto()
> +    #:
> +    intr_lsc = auto()
> +    #:
> +    intr_rmv = auto()
> +    #:
> +    intr_reset = auto()
> +    #:
> +    dev_probed = auto()
> +    #:
> +    dev_released = auto()
> +    #:
> +    flow_aged = auto()
> +    #:
> +    err_recovering = auto()
> +    #:
> +    recovery_success = auto()
> +    #:
> +    recovery_failed = auto()
> +    #:
> +    all = auto()
> +
> +
> +class SimpleMempoolAllocationMode(StrEnum):
> +    """Enum representing simple mempool allocation modes."""
> +
> +    #: Create and populate mempool using native DPDK memory.
> +    native = auto()
> +    #: Create and populate mempool using externally and anonymously allocated area.
> +    xmem = auto()
> +    #: Create and populate mempool using externally and anonymously allocated hugepage area.
> +    xmemhuge = auto()
> +
> +
> +@dataclass(kw_only=True)
> +class AnonMempoolAllocationMode(Params):
> +    """Create mempool using native DPDK memory, but populate using anonymous memory.
> +
> +    Attributes:
> +        no_iova_contig: Enables to create mempool which is not IOVA contiguous.
> +    """
> +
> +    _mp_alloc: Literal["anon"] = field(default="anon", init=False, metadata=Params.long("mp-alloc"))
> +    no_iova_contig: Switch = None
> +
> +
> +@dataclass(slots=True, kw_only=True)
> +class TestPmdParams(EalParams):
> +    """The testpmd shell parameters.
> +
> +    Attributes:
> +        interactive_mode: Runs testpmd in interactive mode.
> +        auto_start: Start forwarding on initialization.
> +        tx_first: Start forwarding, after sending a burst of packets first.
> +        stats_period: Display statistics every ``PERIOD`` seconds, if interactive mode is disabled.
> +                      The default value is 0, which means that the statistics will not be displayed.
> +
> +                      .. note:: This flag should be used only in non-interactive mode.
> +        display_xstats: Display comma-separated list of extended statistics every ``PERIOD`` seconds
> +                        as specified in ``--stats-period`` or when used with interactive commands
> +                        that show Rx/Tx statistics (i.e. ‘show port stats’).
> +        nb_cores: Set the number of forwarding cores, where 1 <= N <= “number of cores” or
> +                  ``RTE_MAX_LCORE`` from the configuration file.
> +        coremask: Set the bitmask of the cores running the packet forwarding test. The main
> +                  lcore is reserved for command line parsing only and cannot be masked on for packet
> +                  forwarding.
> +        nb_ports: Set the number of forwarding ports, where 1 <= N <= “number of ports” on the board
> +                  or ``RTE_MAX_ETHPORTS`` from the configuration file. The default value is the
> +                  number of ports on the board.
> +        port_topology: Set port topology, where mode is paired (the default), chained or loop.
> +        portmask: Set the bitmask of the ports used by the packet forwarding test.
> +        portlist: Set the forwarding ports based on the user input used by the packet forwarding
> +                  test. ‘-‘ denotes a range of ports to set including the two specified port IDs ‘,’
> +                  separates multiple port values. Possible examples like –portlist=0,1 or
> +                  –portlist=0-2 or –portlist=0,1-2 etc.
> +        numa: Enable/disable NUMA-aware allocation of RX/TX rings and of RX memory buffers (mbufs).
> +        socket_num: Set the socket from which all memory is allocated in NUMA mode, where
> +                    0 <= N < number of sockets on the board.
> +        port_numa_config: Specify the socket on which the memory pool to be used by the port will be
> +                          allocated.
> +        ring_numa_config: Specify the socket on which the TX/RX rings for the port will be
> +                          allocated. Where flag is 1 for RX, 2 for TX, and 3 for RX and TX.
> +        total_num_mbufs: Set the number of mbufs to be allocated in the mbuf pools, where N > 1024.
> +        mbuf_size: Set the data size of the mbufs used to N bytes, where N < 65536.
> +                   If multiple mbuf-size values are specified the extra memory pools will be created
> +                   for allocating mbufs to receive packets with buffer splitting features.
> +        mbcache: Set the cache of mbuf memory pools to N, where 0 <= N <= 512.
> +        max_pkt_len: Set the maximum packet size to N bytes, where N >= 64.
> +        eth_peers_configfile: Use a configuration file containing the Ethernet addresses of
> +                              the peer ports.
> +        eth_peer: Set the MAC address XX:XX:XX:XX:XX:XX of the peer port N,
> +                  where 0 <= N < RTE_MAX_ETHPORTS.
> +        tx_ip: Set the source and destination IP address used when doing transmit only test.
> +               The defaults address values are source 198.18.0.1 and destination 198.18.0.2.
> +               These are special purpose addresses reserved for benchmarking (RFC 5735).
> +        tx_udp: Set the source and destination UDP port number for transmit test only test.
> +                The default port is the port 9 which is defined for the discard protocol (RFC 863).
> +        enable_lro: Enable large receive offload.
> +        max_lro_pkt_size: Set the maximum LRO aggregated packet size to N bytes, where N >= 64.
> +        disable_crc_strip: Disable hardware CRC stripping.
> +        enable_scatter: Enable scatter (multi-segment) RX.
> +        enable_hw_vlan: Enable hardware VLAN.
> +        enable_hw_vlan_filter: Enable hardware VLAN filter.
> +        enable_hw_vlan_strip: Enable hardware VLAN strip.
> +        enable_hw_vlan_extend: Enable hardware VLAN extend.
> +        enable_hw_qinq_strip: Enable hardware QINQ strip.
> +        pkt_drop_enabled: Enable per-queue packet drop for packets with no descriptors.
> +        rss: Receive Side Scaling setting.
> +        forward_mode: Set the forwarding mode.
> +        hairpin_mode: Set the hairpin port configuration.
> +        hairpin_queues: Set the number of hairpin queues per port to N, where 1 <= N <= 65535.
> +        burst: Set the number of packets per burst to N, where 1 <= N <= 512.
> +        enable_rx_cksum: Enable hardware RX checksum offload.
> +        rx_queues: Set the number of RX queues per port to N, where 1 <= N <= 65535.
> +        rx_ring: Set the RX rings parameters.
> +        no_flush_rx: Don’t flush the RX streams before starting forwarding. Used mainly with
> +                     the PCAP PMD.
> +        rx_segments_offsets: Set the offsets of packet segments on receiving
> +                             if split feature is engaged.
> +        rx_segments_length: Set the length of segments to scatter packets on receiving
> +                            if split feature is engaged.
> +        multi_rx_mempool: Enable multiple mbuf pools per Rx queue.
> +        rx_shared_queue: Create queues in shared Rx queue mode if device supports. Shared Rx queues
> +                         are grouped per X ports. X defaults to UINT32_MAX, implies all ports join
> +                         share group 1. Forwarding engine “shared-rxq” should be used for shared Rx
> +                         queues. This engine does Rx only and update stream statistics accordingly.
> +        rx_offloads: Set the bitmask of RX queue offloads.
> +        rx_mq_mode: Set the RX multi queue mode which can be enabled.
> +        tx_queues: Set the number of TX queues per port to N, where 1 <= N <= 65535.
> +        tx_ring: Set the TX rings params.
> +        tx_offloads: Set the hexadecimal bitmask of TX queue offloads.
> +        eth_link_speed: Set a forced link speed to the ethernet port. E.g. 1000 for 1Gbps.
> +        disable_link_check: Disable check on link status when starting/stopping ports.
> +        disable_device_start: Do not automatically start all ports. This allows testing
> +                              configuration of rx and tx queues before device is started
> +                              for the first time.
> +        no_lsc_interrupt: Disable LSC interrupts for all ports, even those supporting it.
> +        no_rmv_interrupt: Disable RMV interrupts for all ports, even those supporting it.
> +        bitrate_stats: Set the logical core N to perform bitrate calculation.
> +        latencystats: Set the logical core N to perform latency and jitter calculations.
> +        print_events: Enable printing the occurrence of the designated events.
> +                      Using :attr:`TestPmdEvent.ALL` will enable all of them.
> +        mask_events: Disable printing the occurrence of the designated events.
> +                     Using :attr:`TestPmdEvent.ALL` will disable all of them.
> +        flow_isolate_all: Providing this parameter requests flow API isolated mode on all ports at
> +                          initialization time. It ensures all traffic is received through the
> +                          configured flow rules only (see flow command). Ports that do not support
> +                          this mode are automatically discarded.
> +        disable_flow_flush: Disable port flow flush when stopping port.
> +                            This allows testing keep flow rules or shared flow objects across
> +                            restart.
> +        hot_plug: Enable device event monitor mechanism for hotplug.
> +        vxlan_gpe_port: Set the UDP port number of tunnel VXLAN-GPE to N.
> +        geneve_parsed_port: Set the UDP port number that is used for parsing the GENEVE protocol
> +                            to N. HW may be configured with another tunnel Geneve port.
> +        lock_all_memory: Enable/disable locking all memory. Disabled by default.
> +        mempool_allocation_mode: Set mempool allocation mode.
> +        record_core_cycles: Enable measurement of CPU cycles per packet.
> +        record_burst_status: Enable display of RX and TX burst stats.
> +    """
> +
> +    interactive_mode: Switch = field(default=True, metadata=Params.short("i"))
> +    auto_start: Switch = field(default=None, metadata=Params.short("a"))
> +    tx_first: Switch = None
> +    stats_period: int | None = None
> +    display_xstats: list[str] | None = field(
> +        default=None, metadata=Params.convert_value(comma_separated)
> +    )
> +    nb_cores: int | None = None
> +    coremask: int | None = field(default=None, metadata=Params.convert_value(hex))
> +    nb_ports: int | None = None
> +    port_topology: PortTopology | None = PortTopology.paired
> +    portmask: int | None = field(default=None, metadata=Params.convert_value(hex))
> +    portlist: str | None = None  # TODO: can be ranges 0,1-3
> +
> +    numa: YesNoSwitch = True
> +    socket_num: int | None = None
> +    port_numa_config: list[PortNUMAConfig] | None = field(
> +        default=None, metadata=Params.convert_value(comma_separated)
> +    )
> +    ring_numa_config: list[RingNUMAConfig] | None = field(
> +        default=None, metadata=Params.convert_value(comma_separated)
> +    )
> +    total_num_mbufs: int | None = None
> +    mbuf_size: list[int] | None = field(
> +        default=None, metadata=Params.convert_value(comma_separated)
> +    )
> +    mbcache: int | None = None
> +    max_pkt_len: int | None = None
> +    eth_peers_configfile: PurePath | None = None
> +    eth_peer: list[EthPeer] | None = field(default=None, metadata=Params.multiple())
> +    tx_ip: TxIPAddrPair | None = TxIPAddrPair(source_ip="198.18.0.1", dest_ip="198.18.0.2")
> +    tx_udp: TxUDPPortPair | None = TxUDPPortPair(9)
> +    enable_lro: Switch = None
> +    max_lro_pkt_size: int | None = None
> +    disable_crc_strip: Switch = None
> +    enable_scatter: Switch = None
> +    enable_hw_vlan: Switch = None
> +    enable_hw_vlan_filter: Switch = None
> +    enable_hw_vlan_strip: Switch = None
> +    enable_hw_vlan_extend: Switch = None
> +    enable_hw_qinq_strip: Switch = None
> +    pkt_drop_enabled: Switch = field(default=None, metadata=Params.long("enable-drop-en"))
> +    rss: RSSSetting | None = None
> +    forward_mode: (
> +        SimpleForwardingModes
> +        | FlowGenForwardingMode
> +        | TXOnlyForwardingMode
> +        | NoisyForwardingMode
> +        | None
> +    ) = SimpleForwardingModes.io
> +    hairpin_mode: HairpinMode | None = HairpinMode(0)
> +    hairpin_queues: int | None = field(default=None, metadata=Params.long("hairpinq"))
> +    burst: int | None = None
> +    enable_rx_cksum: Switch = None
> +
> +    rx_queues: int | None = field(default=None, metadata=Params.long("rxq"))
> +    rx_ring: RXRingParams | None = None
> +    no_flush_rx: Switch = None
> +    rx_segments_offsets: list[int] | None = field(
> +        default=None, metadata=Params.long("rxoffs") | Params.convert_value(comma_separated)
> +    )
> +    rx_segments_length: list[int] | None = field(
> +        default=None, metadata=Params.long("rxpkts") | Params.convert_value(comma_separated)
> +    )
> +    multi_rx_mempool: Switch = None
> +    rx_shared_queue: Switch | int = field(default=None, metadata=Params.long("rxq-share"))
> +    rx_offloads: int | None = field(default=None, metadata=Params.convert_value(hex))
> +    rx_mq_mode: RXMultiQueueMode | None = (
> +        RXMultiQueueMode.DCB | RXMultiQueueMode.RSS | RXMultiQueueMode.VMDQ
> +    )
> +
> +    tx_queues: int | None = field(default=None, metadata=Params.long("txq"))
> +    tx_ring: TXRingParams | None = None
> +    tx_offloads: int | None = field(default=None, metadata=Params.convert_value(hex))
> +
> +    eth_link_speed: int | None = None
> +    disable_link_check: Switch = None
> +    disable_device_start: Switch = None
> +    no_lsc_interrupt: Switch = None
> +    no_rmv_interrupt: Switch = None
> +    bitrate_stats: int | None = None
> +    latencystats: int | None = None
> +    print_events: list[Event] | None = field(
> +        default=None, metadata=Params.multiple() | Params.long("print-event")
> +    )
> +    mask_events: list[Event] | None = field(
> +        default_factory=lambda: [Event.intr_lsc],
> +        metadata=Params.multiple() | Params.long("mask-event"),
> +    )
> +
> +    flow_isolate_all: Switch = None
> +    disable_flow_flush: Switch = None
> +
> +    hot_plug: Switch = None
> +    vxlan_gpe_port: int | None = None
> +    geneve_parsed_port: int | None = None
> +    lock_all_memory: YesNoSwitch = field(default=False, metadata=Params.long("mlockall"))
> +    mempool_allocation_mode: SimpleMempoolAllocationMode | AnonMempoolAllocationMode | None = field(
> +        default=None, metadata=Params.long("mp-alloc")
> +    )
> +    record_core_cycles: Switch = None
> +    record_burst_status: Switch = None
> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
> index 841d456a2f..ef3f23c582 100644
> --- a/dts/framework/remote_session/testpmd_shell.py
> +++ b/dts/framework/remote_session/testpmd_shell.py
> @@ -1,6 +1,7 @@
>  # SPDX-License-Identifier: BSD-3-Clause
>  # Copyright(c) 2023 University of New Hampshire
>  # Copyright(c) 2023 PANTHEON.tech s.r.o.
> +# Copyright(c) 2024 Arm Limited
>
>  """Testpmd interactive shell.
>
> @@ -16,14 +17,12 @@
>  """
>
>  import time
> -from enum import auto
>  from pathlib import PurePath
>  from typing import Callable, ClassVar
>
>  from framework.exception import InteractiveCommandExecutionError
> -from framework.params.eal import EalParams
> +from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
>  from framework.settings import SETTINGS
> -from framework.utils import StrEnum
>
>  from .interactive_shell import InteractiveShell
>
> @@ -50,37 +49,6 @@ def __str__(self) -> str:
>          return self.pci_address
>
>
> -class TestPmdForwardingModes(StrEnum):
> -    r"""The supported packet forwarding modes for :class:`~TestPmdShell`\s."""
> -
> -    #:
> -    io = auto()
> -    #:
> -    mac = auto()
> -    #:
> -    macswap = auto()
> -    #:
> -    flowgen = auto()
> -    #:
> -    rxonly = auto()
> -    #:
> -    txonly = auto()
> -    #:
> -    csum = auto()
> -    #:
> -    icmpecho = auto()
> -    #:
> -    ieee1588 = auto()
> -    #:
> -    noisy = auto()
> -    #:
> -    fivetswap = "5tswap"
> -    #:
> -    shared_rxq = "shared-rxq"
> -    #:
> -    recycle_mbufs = auto()
> -
> -
>  class TestPmdShell(InteractiveShell):
>      """Testpmd interactive shell.
>
> @@ -119,9 +87,7 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
>          Also find the number of pci addresses which were allowed on the command line when the app
>          was started.
>          """
> -        self._app_params += " -i --mask-event intr_lsc"
> -
> -        assert isinstance(self._app_params, EalParams)
> +        assert isinstance(self._app_params, TestPmdParams)
>
>          self.number_of_ports = (
>              len(self._app_params.ports) if self._app_params.ports is not None else 0
> @@ -213,7 +179,7 @@ def wait_link_status_up(self, port_id: int, timeout=SETTINGS.timeout) -> bool:
>              self._logger.error(f"The link for port {port_id} did not come up in the given timeout.")
>          return "Link status: up" in port_info
>
> -    def set_forward_mode(self, mode: TestPmdForwardingModes, verify: bool = True):
> +    def set_forward_mode(self, mode: SimpleForwardingModes, verify: bool = True):
>          """Set packet forwarding mode.
>
>          Args:
> diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
> index c6e93839cb..578b5a4318 100644
> --- a/dts/tests/TestSuite_pmd_buffer_scatter.py
> +++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
> @@ -23,7 +23,8 @@
>  from scapy.utils import hexstr  # type: ignore[import-untyped]
>
>  from framework.params import Params
> -from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
> +from framework.params.testpmd import SimpleForwardingModes
> +from framework.remote_session.testpmd_shell import TestPmdShell
>  from framework.test_suite import TestSuite
>
>
> @@ -113,7 +114,7 @@ def pmd_scatter(self, mbsize: int) -> None:
>              ),
>              privileged=True,
>          )
> -        testpmd.set_forward_mode(TestPmdForwardingModes.mac)
> +        testpmd.set_forward_mode(SimpleForwardingModes.mac)
>          testpmd.start()
>
>          for offset in [-1, 0, 1, 4, 5]:
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 4/8] dts: remove module-wide imports
  2024-05-30 15:25   ` [PATCH v3 4/8] dts: remove module-wide imports Luca Vizzarro
  2024-05-30 20:12     ` Jeremy Spewock
@ 2024-05-31 15:21     ` Nicholas Pratte
  1 sibling, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-05-31 15:21 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Jeremy Spewock, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Thu, May 30, 2024 at 11:25 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Remove the imports in the testbed_model and remote_session modules init
> file, to avoid the initialisation of unneeded modules, thus removing or
> limiting the risk of circular dependencies.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>  dts/framework/remote_session/__init__.py               | 5 +----
>  dts/framework/runner.py                                | 4 +++-
>  dts/framework/test_suite.py                            | 5 ++++-
>  dts/framework/testbed_model/__init__.py                | 7 -------
>  dts/framework/testbed_model/os_session.py              | 4 ++--
>  dts/framework/testbed_model/sut_node.py                | 2 +-
>  dts/framework/testbed_model/traffic_generator/scapy.py | 2 +-
>  dts/tests/TestSuite_hello_world.py                     | 2 +-
>  dts/tests/TestSuite_smoke_tests.py                     | 2 +-
>  9 files changed, 14 insertions(+), 19 deletions(-)
>
> diff --git a/dts/framework/remote_session/__init__.py b/dts/framework/remote_session/__init__.py
> index 1910c81c3c..29000a4642 100644
> --- a/dts/framework/remote_session/__init__.py
> +++ b/dts/framework/remote_session/__init__.py
> @@ -18,11 +18,8 @@
>  from framework.logger import DTSLogger
>
>  from .interactive_remote_session import InteractiveRemoteSession
> -from .interactive_shell import InteractiveShell
> -from .python_shell import PythonShell
> -from .remote_session import CommandResult, RemoteSession
> +from .remote_session import RemoteSession
>  from .ssh_session import SSHSession
> -from .testpmd_shell import TestPmdShell
>
>
>  def create_remote_session(
> diff --git a/dts/framework/runner.py b/dts/framework/runner.py
> index dfdee14802..687bc04f79 100644
> --- a/dts/framework/runner.py
> +++ b/dts/framework/runner.py
> @@ -26,6 +26,9 @@
>  from types import FunctionType
>  from typing import Iterable, Sequence
>
> +from framework.testbed_model.sut_node import SutNode
> +from framework.testbed_model.tg_node import TGNode
> +
>  from .config import (
>      BuildTargetConfiguration,
>      Configuration,
> @@ -51,7 +54,6 @@
>      TestSuiteWithCases,
>  )
>  from .test_suite import TestSuite
> -from .testbed_model import SutNode, TGNode
>
>
>  class DTSRunner:
> diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py
> index 8768f756a6..9d3debb00f 100644
> --- a/dts/framework/test_suite.py
> +++ b/dts/framework/test_suite.py
> @@ -20,9 +20,12 @@
>  from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
>  from scapy.packet import Packet, Padding  # type: ignore[import-untyped]
>
> +from framework.testbed_model.port import Port, PortLink
> +from framework.testbed_model.sut_node import SutNode
> +from framework.testbed_model.tg_node import TGNode
> +
>  from .exception import TestCaseVerifyError
>  from .logger import DTSLogger, get_dts_logger
> -from .testbed_model import Port, PortLink, SutNode, TGNode
>  from .testbed_model.traffic_generator import PacketFilteringConfig
>  from .utils import get_packet_summaries
>
> diff --git a/dts/framework/testbed_model/__init__.py b/dts/framework/testbed_model/__init__.py
> index 6086512ca2..4f8a58c039 100644
> --- a/dts/framework/testbed_model/__init__.py
> +++ b/dts/framework/testbed_model/__init__.py
> @@ -19,10 +19,3 @@
>  """
>
>  # pylama:ignore=W0611
> -
> -from .cpu import LogicalCoreCount, LogicalCoreCountFilter, LogicalCoreList
> -from .node import Node
> -from .port import Port, PortLink
> -from .sut_node import SutNode
> -from .tg_node import TGNode
> -from .virtual_device import VirtualDevice
> diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
> index 1a77aee532..e5f5fcbe0e 100644
> --- a/dts/framework/testbed_model/os_session.py
> +++ b/dts/framework/testbed_model/os_session.py
> @@ -32,13 +32,13 @@
>  from framework.logger import DTSLogger
>  from framework.params import Params
>  from framework.remote_session import (
> -    CommandResult,
>      InteractiveRemoteSession,
> -    InteractiveShell,
>      RemoteSession,
>      create_interactive_session,
>      create_remote_session,
>  )
> +from framework.remote_session.interactive_shell import InteractiveShell
> +from framework.remote_session.remote_session import CommandResult
>  from framework.settings import SETTINGS
>  from framework.utils import MesonArgs
>
> diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
> index e1163106a3..83ad06ae2d 100644
> --- a/dts/framework/testbed_model/sut_node.py
> +++ b/dts/framework/testbed_model/sut_node.py
> @@ -26,7 +26,7 @@
>  )
>  from framework.params import Params, Switch
>  from framework.params.eal import EalParams
> -from framework.remote_session import CommandResult
> +from framework.remote_session.remote_session import CommandResult
>  from framework.settings import SETTINGS
>  from framework.utils import MesonArgs
>
> diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py
> index ed5467d825..7bc1c2cc08 100644
> --- a/dts/framework/testbed_model/traffic_generator/scapy.py
> +++ b/dts/framework/testbed_model/traffic_generator/scapy.py
> @@ -25,7 +25,7 @@
>  from scapy.packet import Packet  # type: ignore[import-untyped]
>
>  from framework.config import OS, ScapyTrafficGeneratorConfig
> -from framework.remote_session import PythonShell
> +from framework.remote_session.python_shell import PythonShell
>  from framework.settings import SETTINGS
>  from framework.testbed_model.node import Node
>  from framework.testbed_model.port import Port
> diff --git a/dts/tests/TestSuite_hello_world.py b/dts/tests/TestSuite_hello_world.py
> index fd7ff1534d..0d6995f260 100644
> --- a/dts/tests/TestSuite_hello_world.py
> +++ b/dts/tests/TestSuite_hello_world.py
> @@ -8,7 +8,7 @@
>  """
>
>  from framework.test_suite import TestSuite
> -from framework.testbed_model import (
> +from framework.testbed_model.cpu import (
>      LogicalCoreCount,
>      LogicalCoreCountFilter,
>      LogicalCoreList,
> diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py
> index a553e89662..ca678f662d 100644
> --- a/dts/tests/TestSuite_smoke_tests.py
> +++ b/dts/tests/TestSuite_smoke_tests.py
> @@ -15,7 +15,7 @@
>  import re
>
>  from framework.config import PortConfig
> -from framework.remote_session import TestPmdShell
> +from framework.remote_session.testpmd_shell import TestPmdShell
>  from framework.settings import SETTINGS
>  from framework.test_suite import TestSuite
>  from framework.utils import REGEX_FOR_PCI_ADDRESS
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 3/8] dts: refactor EalParams
  2024-05-30 15:25   ` [PATCH v3 3/8] dts: refactor EalParams Luca Vizzarro
  2024-05-30 20:12     ` Jeremy Spewock
@ 2024-05-31 15:21     ` Nicholas Pratte
  1 sibling, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-05-31 15:21 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Jeremy Spewock, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Thu, May 30, 2024 at 11:25 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Move EalParams to its own module to avoid circular dependencies.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>  dts/framework/params/eal.py                   | 50 +++++++++++++++++++
>  dts/framework/remote_session/testpmd_shell.py |  2 +-
>  dts/framework/testbed_model/sut_node.py       | 42 +---------------
>  3 files changed, 53 insertions(+), 41 deletions(-)
>  create mode 100644 dts/framework/params/eal.py
>
> diff --git a/dts/framework/params/eal.py b/dts/framework/params/eal.py
> new file mode 100644
> index 0000000000..bbdbc8f334
> --- /dev/null
> +++ b/dts/framework/params/eal.py
> @@ -0,0 +1,50 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2024 Arm Limited
> +
> +"""Module representing the DPDK EAL-related parameters."""
> +
> +from dataclasses import dataclass, field
> +from typing import Literal
> +
> +from framework.params import Params, Switch
> +from framework.testbed_model.cpu import LogicalCoreList
> +from framework.testbed_model.port import Port
> +from framework.testbed_model.virtual_device import VirtualDevice
> +
> +
> +def _port_to_pci(port: Port) -> str:
> +    return port.pci
> +
> +
> +@dataclass(kw_only=True)
> +class EalParams(Params):
> +    """The environment abstraction layer parameters.
> +
> +    Attributes:
> +        lcore_list: The list of logical cores to use.
> +        memory_channels: The number of memory channels to use.
> +        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
> +        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
> +        vdevs: Virtual devices, e.g.::
> +            vdevs=[
> +                VirtualDevice('net_ring0'),
> +                VirtualDevice('net_ring1')
> +            ]
> +        ports: The list of ports to allow.
> +        other_eal_param: user defined DPDK EAL parameters, e.g.:
> +                ``other_eal_param='--single-file-segments'``
> +    """
> +
> +    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
> +    memory_channels: int = field(metadata=Params.short("n"))
> +    prefix: str = field(metadata=Params.long("file-prefix"))
> +    no_pci: Switch = None
> +    vdevs: list[VirtualDevice] | None = field(
> +        default=None, metadata=Params.multiple() | Params.long("vdev")
> +    )
> +    ports: list[Port] | None = field(
> +        default=None,
> +        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
> +    )
> +    other_eal_param: Params | None = None
> +    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
> index 7eced27096..841d456a2f 100644
> --- a/dts/framework/remote_session/testpmd_shell.py
> +++ b/dts/framework/remote_session/testpmd_shell.py
> @@ -21,8 +21,8 @@
>  from typing import Callable, ClassVar
>
>  from framework.exception import InteractiveCommandExecutionError
> +from framework.params.eal import EalParams
>  from framework.settings import SETTINGS
> -from framework.testbed_model.sut_node import EalParams
>  from framework.utils import StrEnum
>
>  from .interactive_shell import InteractiveShell
> diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
> index c886590979..e1163106a3 100644
> --- a/dts/framework/testbed_model/sut_node.py
> +++ b/dts/framework/testbed_model/sut_node.py
> @@ -15,9 +15,8 @@
>  import os
>  import tarfile
>  import time
> -from dataclasses import dataclass, field
>  from pathlib import PurePath
> -from typing import Literal, Type
> +from typing import Type
>
>  from framework.config import (
>      BuildTargetConfiguration,
> @@ -26,6 +25,7 @@
>      SutNodeConfiguration,
>  )
>  from framework.params import Params, Switch
> +from framework.params.eal import EalParams
>  from framework.remote_session import CommandResult
>  from framework.settings import SETTINGS
>  from framework.utils import MesonArgs
> @@ -37,44 +37,6 @@
>  from .virtual_device import VirtualDevice
>
>
> -def _port_to_pci(port: Port) -> str:
> -    return port.pci
> -
> -
> -@dataclass(kw_only=True)
> -class EalParams(Params):
> -    """The environment abstraction layer parameters.
> -
> -    Attributes:
> -        lcore_list: The list of logical cores to use.
> -        memory_channels: The number of memory channels to use.
> -        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
> -        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
> -        vdevs: Virtual devices, e.g.::
> -            vdevs=[
> -                VirtualDevice('net_ring0'),
> -                VirtualDevice('net_ring1')
> -            ]
> -        ports: The list of ports to allow.
> -        other_eal_param: user defined DPDK EAL parameters, e.g.:
> -                ``other_eal_param='--single-file-segments'``
> -    """
> -
> -    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
> -    memory_channels: int = field(metadata=Params.short("n"))
> -    prefix: str = field(metadata=Params.long("file-prefix"))
> -    no_pci: Switch
> -    vdevs: list[VirtualDevice] | None = field(
> -        default=None, metadata=Params.multiple() | Params.long("vdev")
> -    )
> -    ports: list[Port] | None = field(
> -        default=None,
> -        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
> -    )
> -    other_eal_param: Params | None = None
> -    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
> -
> -
>  class SutNode(Node):
>      """The system under test node.
>
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 8/8] dts: use Unpack for type checking and hinting
  2024-05-30 15:25   ` [PATCH v3 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
  2024-05-30 20:13     ` Jeremy Spewock
@ 2024-05-31 15:21     ` Nicholas Pratte
  2024-06-06 18:05     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-05-31 15:21 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Jeremy Spewock, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Thu, May 30, 2024 at 11:25 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Interactive shells that inherit DPDKShell initialise their params
> classes from a kwargs dict. Therefore, static type checking is
> disabled. This change uses the functionality of Unpack added in
> PEP 692 to re-enable it. The disadvantage is that this functionality has
> been implemented only with TypedDict, forcing the creation of TypedDict
> mirrors of the Params classes.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>  dts/framework/params/types.py                 | 133 ++++++++++++++++++
>  dts/framework/remote_session/testpmd_shell.py |   5 +-
>  2 files changed, 137 insertions(+), 1 deletion(-)
>  create mode 100644 dts/framework/params/types.py
>
> diff --git a/dts/framework/params/types.py b/dts/framework/params/types.py
> new file mode 100644
> index 0000000000..e668f658d8
> --- /dev/null
> +++ b/dts/framework/params/types.py
> @@ -0,0 +1,133 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2024 Arm Limited
> +
> +"""Module containing TypeDict-equivalents of Params classes for static typing and hinting.
> +
> +TypedDicts can be used in conjunction with Unpack and kwargs for type hinting on function calls.
> +
> +Example:
> +    ..code:: python
> +        def create_testpmd(**kwargs: Unpack[TestPmdParamsDict]):
> +            params = TestPmdParams(**kwargs)
> +"""
> +
> +from pathlib import PurePath
> +from typing import TypedDict
> +
> +from framework.params import Switch, YesNoSwitch
> +from framework.params.testpmd import (
> +    AnonMempoolAllocationMode,
> +    EthPeer,
> +    Event,
> +    FlowGenForwardingMode,
> +    HairpinMode,
> +    NoisyForwardingMode,
> +    Params,
> +    PortNUMAConfig,
> +    PortTopology,
> +    RingNUMAConfig,
> +    RSSSetting,
> +    RXMultiQueueMode,
> +    RXRingParams,
> +    SimpleForwardingModes,
> +    SimpleMempoolAllocationMode,
> +    TxIPAddrPair,
> +    TXOnlyForwardingMode,
> +    TXRingParams,
> +    TxUDPPortPair,
> +)
> +from framework.testbed_model.cpu import LogicalCoreList
> +from framework.testbed_model.port import Port
> +from framework.testbed_model.virtual_device import VirtualDevice
> +
> +
> +class EalParamsDict(TypedDict, total=False):
> +    """:class:`TypedDict` equivalent of :class:`~.eal.EalParams`."""
> +
> +    lcore_list: LogicalCoreList | None
> +    memory_channels: int | None
> +    prefix: str
> +    no_pci: Switch
> +    vdevs: list[VirtualDevice] | None
> +    ports: list[Port] | None
> +    other_eal_param: Params | None
> +
> +
> +class TestPmdParamsDict(EalParamsDict, total=False):
> +    """:class:`TypedDict` equivalent of :class:`~.testpmd.TestPmdParams`."""
> +
> +    interactive_mode: Switch
> +    auto_start: Switch
> +    tx_first: Switch
> +    stats_period: int | None
> +    display_xstats: list[str] | None
> +    nb_cores: int | None
> +    coremask: int | None
> +    nb_ports: int | None
> +    port_topology: PortTopology | None
> +    portmask: int | None
> +    portlist: str | None
> +    numa: YesNoSwitch
> +    socket_num: int | None
> +    port_numa_config: list[PortNUMAConfig] | None
> +    ring_numa_config: list[RingNUMAConfig] | None
> +    total_num_mbufs: int | None
> +    mbuf_size: list[int] | None
> +    mbcache: int | None
> +    max_pkt_len: int | None
> +    eth_peers_configfile: PurePath | None
> +    eth_peer: list[EthPeer] | None
> +    tx_ip: TxIPAddrPair | None
> +    tx_udp: TxUDPPortPair | None
> +    enable_lro: Switch
> +    max_lro_pkt_size: int | None
> +    disable_crc_strip: Switch
> +    enable_scatter: Switch
> +    enable_hw_vlan: Switch
> +    enable_hw_vlan_filter: Switch
> +    enable_hw_vlan_strip: Switch
> +    enable_hw_vlan_extend: Switch
> +    enable_hw_qinq_strip: Switch
> +    pkt_drop_enabled: Switch
> +    rss: RSSSetting | None
> +    forward_mode: (
> +        SimpleForwardingModes
> +        | FlowGenForwardingMode
> +        | TXOnlyForwardingMode
> +        | NoisyForwardingMode
> +        | None
> +    )
> +    hairpin_mode: HairpinMode | None
> +    hairpin_queues: int | None
> +    burst: int | None
> +    enable_rx_cksum: Switch
> +    rx_queues: int | None
> +    rx_ring: RXRingParams | None
> +    no_flush_rx: Switch
> +    rx_segments_offsets: list[int] | None
> +    rx_segments_length: list[int] | None
> +    multi_rx_mempool: Switch
> +    rx_shared_queue: Switch | int
> +    rx_offloads: int | None
> +    rx_mq_mode: RXMultiQueueMode | None
> +    tx_queues: int | None
> +    tx_ring: TXRingParams | None
> +    tx_offloads: int | None
> +    eth_link_speed: int | None
> +    disable_link_check: Switch
> +    disable_device_start: Switch
> +    no_lsc_interrupt: Switch
> +    no_rmv_interrupt: Switch
> +    bitrate_stats: int | None
> +    latencystats: int | None
> +    print_events: list[Event] | None
> +    mask_events: list[Event] | None
> +    flow_isolate_all: Switch
> +    disable_flow_flush: Switch
> +    hot_plug: Switch
> +    vxlan_gpe_port: int | None
> +    geneve_parsed_port: int | None
> +    lock_all_memory: YesNoSwitch
> +    mempool_allocation_mode: SimpleMempoolAllocationMode | AnonMempoolAllocationMode | None
> +    record_core_cycles: Switch
> +    record_burst_status: Switch
> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
> index 39985000b9..4114f946a8 100644
> --- a/dts/framework/remote_session/testpmd_shell.py
> +++ b/dts/framework/remote_session/testpmd_shell.py
> @@ -18,8 +18,11 @@
>  from pathlib import PurePath
>  from typing import ClassVar
>
> +from typing_extensions import Unpack
> +
>  from framework.exception import InteractiveCommandExecutionError
>  from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
> +from framework.params.types import TestPmdParamsDict
>  from framework.remote_session.dpdk_shell import DPDKShell
>  from framework.settings import SETTINGS
>  from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
> @@ -76,7 +79,7 @@ def __init__(
>          ascending_cores: bool = True,
>          append_prefix_timestamp: bool = True,
>          start_on_init: bool = True,
> -        **app_params,
> +        **app_params: Unpack[TestPmdParamsDict],
>      ) -> None:
>          """Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs."""
>          super().__init__(
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 7/8] dts: rework interactive shells
  2024-05-30 15:25   ` [PATCH v3 7/8] dts: rework interactive shells Luca Vizzarro
  2024-05-30 20:13     ` Jeremy Spewock
@ 2024-05-31 15:22     ` Nicholas Pratte
  2024-06-06 18:03     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-05-31 15:22 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Jeremy Spewock, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Thu, May 30, 2024 at 11:25 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> The way nodes and interactive shells interact makes it difficult to
> develop for static type checking and hinting. The current system relies
> on a top-down approach, attempting to give a generic interface to the
> test developer, hiding the interaction of concrete shell classes as much
> as possible. When working with strong typing this approach is not ideal,
> as Python's implementation of generics is still rudimentary.
>
> This rework reverses the tests interaction to a bottom-up approach,
> allowing the test developer to call concrete shell classes directly,
> and let them ingest nodes independently. While also re-enforcing type
> checking and making the code easier to read.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>  dts/framework/params/eal.py                   |   6 +-
>  dts/framework/remote_session/dpdk_shell.py    | 104 ++++++++++++++++
>  .../remote_session/interactive_shell.py       |  74 +++++++-----
>  dts/framework/remote_session/python_shell.py  |   4 +-
>  dts/framework/remote_session/testpmd_shell.py |  64 +++++-----
>  dts/framework/testbed_model/node.py           |  36 +-----
>  dts/framework/testbed_model/os_session.py     |  36 +-----
>  dts/framework/testbed_model/sut_node.py       | 112 +-----------------
>  .../testbed_model/traffic_generator/scapy.py  |   4 +-
>  dts/tests/TestSuite_hello_world.py            |   7 +-
>  dts/tests/TestSuite_pmd_buffer_scatter.py     |  21 ++--
>  dts/tests/TestSuite_smoke_tests.py            |   2 +-
>  12 files changed, 200 insertions(+), 270 deletions(-)
>  create mode 100644 dts/framework/remote_session/dpdk_shell.py
>
> diff --git a/dts/framework/params/eal.py b/dts/framework/params/eal.py
> index bbdbc8f334..8d7766fefc 100644
> --- a/dts/framework/params/eal.py
> +++ b/dts/framework/params/eal.py
> @@ -35,9 +35,9 @@ class EalParams(Params):
>                  ``other_eal_param='--single-file-segments'``
>      """
>
> -    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
> -    memory_channels: int = field(metadata=Params.short("n"))
> -    prefix: str = field(metadata=Params.long("file-prefix"))
> +    lcore_list: LogicalCoreList | None = field(default=None, metadata=Params.short("l"))
> +    memory_channels: int | None = field(default=None, metadata=Params.short("n"))
> +    prefix: str = field(default="dpdk", metadata=Params.long("file-prefix"))
>      no_pci: Switch = None
>      vdevs: list[VirtualDevice] | None = field(
>          default=None, metadata=Params.multiple() | Params.long("vdev")
> diff --git a/dts/framework/remote_session/dpdk_shell.py b/dts/framework/remote_session/dpdk_shell.py
> new file mode 100644
> index 0000000000..25e3df4eaa
> --- /dev/null
> +++ b/dts/framework/remote_session/dpdk_shell.py
> @@ -0,0 +1,104 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2024 Arm Limited
> +
> +"""DPDK-based interactive shell.
> +
> +Provides a base class to create interactive shells based on DPDK.
> +"""
> +
> +
> +from abc import ABC
> +
> +from framework.params.eal import EalParams
> +from framework.remote_session.interactive_shell import InteractiveShell
> +from framework.settings import SETTINGS
> +from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
> +from framework.testbed_model.sut_node import SutNode
> +
> +
> +def compute_eal_params(
> +    node: SutNode,
> +    params: EalParams | None = None,
> +    lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
> +    ascending_cores: bool = True,
> +    append_prefix_timestamp: bool = True,
> +) -> EalParams:
> +    """Compute EAL parameters based on the node's specifications.
> +
> +    Args:
> +        node: The SUT node to compute the values for.
> +        params: The EalParams object to amend, if set to None a new object is created and returned.
> +        lcore_filter_specifier: A number of lcores/cores/sockets to use
> +            or a list of lcore ids to use.
> +            The default will select one lcore for each of two cores
> +            on one socket, in ascending order of core ids.
> +        ascending_cores: Sort cores in ascending order (lowest to highest IDs).
> +            If :data:`False`, sort in descending order.
> +        append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.
> +    """
> +    if params is None:
> +        params = EalParams()
> +
> +    if params.lcore_list is None:
> +        params.lcore_list = LogicalCoreList(
> +            node.filter_lcores(lcore_filter_specifier, ascending_cores)
> +        )
> +
> +    prefix = params.prefix
> +    if append_prefix_timestamp:
> +        prefix = f"{prefix}_{node._dpdk_timestamp}"
> +    prefix = node.main_session.get_dpdk_file_prefix(prefix)
> +    if prefix:
> +        node._dpdk_prefix_list.append(prefix)
> +    params.prefix = prefix
> +
> +    if params.ports is None:
> +        params.ports = node.ports
> +
> +    return params
> +
> +
> +class DPDKShell(InteractiveShell, ABC):
> +    """The base class for managing DPDK-based interactive shells.
> +
> +    This class shouldn't be instantiated directly, but instead be extended.
> +    It automatically injects computed EAL parameters based on the node in the
> +    supplied app parameters.
> +    """
> +
> +    _node: SutNode
> +    _app_params: EalParams
> +
> +    _lcore_filter_specifier: LogicalCoreCount | LogicalCoreList
> +    _ascending_cores: bool
> +    _append_prefix_timestamp: bool
> +
> +    def __init__(
> +        self,
> +        node: SutNode,
> +        app_params: EalParams,
> +        privileged: bool = True,
> +        timeout: float = SETTINGS.timeout,
> +        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
> +        ascending_cores: bool = True,
> +        append_prefix_timestamp: bool = True,
> +        start_on_init: bool = True,
> +    ) -> None:
> +        """Overrides :meth:`~.interactive_shell.InteractiveShell.__init__`."""
> +        self._lcore_filter_specifier = lcore_filter_specifier
> +        self._ascending_cores = ascending_cores
> +        self._append_prefix_timestamp = append_prefix_timestamp
> +
> +        super().__init__(node, app_params, privileged, timeout, start_on_init)
> +
> +    def _post_init(self):
> +        """Computes EAL params based on the node capabilities before start."""
> +        self._app_params = compute_eal_params(
> +            self._node,
> +            self._app_params,
> +            self._lcore_filter_specifier,
> +            self._ascending_cores,
> +            self._append_prefix_timestamp,
> +        )
> +
> +        self._update_path(self._node.remote_dpdk_build_dir.joinpath(self.path))
> diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
> index 9da66d1c7e..4be7966672 100644
> --- a/dts/framework/remote_session/interactive_shell.py
> +++ b/dts/framework/remote_session/interactive_shell.py
> @@ -17,13 +17,14 @@
>
>  from abc import ABC
>  from pathlib import PurePath
> -from typing import Callable, ClassVar
> +from typing import ClassVar
>
> -from paramiko import Channel, SSHClient, channel  # type: ignore[import-untyped]
> +from paramiko import Channel, channel  # type: ignore[import-untyped]
>
>  from framework.logger import DTSLogger
>  from framework.params import Params
>  from framework.settings import SETTINGS
> +from framework.testbed_model.node import Node
>
>
>  class InteractiveShell(ABC):
> @@ -36,13 +37,14 @@ class InteractiveShell(ABC):
>      session.
>      """
>
> -    _interactive_session: SSHClient
> +    _node: Node
>      _stdin: channel.ChannelStdinFile
>      _stdout: channel.ChannelFile
>      _ssh_channel: Channel
>      _logger: DTSLogger
>      _timeout: float
>      _app_params: Params
> +    _privileged: bool
>
>      #: Prompt to expect at the end of output when sending a command.
>      #: This is often overridden by subclasses.
> @@ -56,55 +58,63 @@ class InteractiveShell(ABC):
>      #: Path to the executable to start the interactive application.
>      path: ClassVar[PurePath]
>
> -    #: Whether this application is a DPDK app. If it is, the build directory
> -    #: for DPDK on the node will be prepended to the path to the executable.
> -    dpdk_app: ClassVar[bool] = False
> -
>      def __init__(
>          self,
> -        interactive_session: SSHClient,
> -        logger: DTSLogger,
> -        get_privileged_command: Callable[[str], str] | None,
> +        node: Node,
>          app_params: Params = Params(),
> +        privileged: bool = False,
>          timeout: float = SETTINGS.timeout,
> +        start_on_init: bool = True,
>      ) -> None:
>          """Create an SSH channel during initialization.
>
>          Args:
> -            interactive_session: The SSH session dedicated to interactive shells.
> -            logger: The logger instance this session will use.
> -            get_privileged_command: A method for modifying a command to allow it to use
> -                elevated privileges. If :data:`None`, the application will not be started
> -                with elevated privileges.
> +            node: The node on which to run start the interactive shell.
>              app_params: The command line parameters to be passed to the application on startup.
> +            privileged: Enables the shell to run as superuser.
>              timeout: The timeout used for the SSH channel that is dedicated to this interactive
>                  shell. This timeout is for collecting output, so if reading from the buffer
>                  and no output is gathered within the timeout, an exception is thrown.
> +            start_on_init: Start interactive shell automatically after object initialisation.
>          """
> -        self._interactive_session = interactive_session
> -        self._ssh_channel = self._interactive_session.invoke_shell()
> +        self._node = node
> +        self._logger = node._logger
> +        self._app_params = app_params
> +        self._privileged = privileged
> +        self._timeout = timeout
> +        # Ensure path is properly formatted for the host
> +        self._update_path(self._node.main_session.join_remote_path(self.path))
> +
> +        self._post_init()
> +
> +        if start_on_init:
> +            self.start_application()
> +
> +    def _post_init(self):
> +        """Overridable. Method called after the object init and before application start."""
> +
> +    def _setup_ssh_channel(self):
> +        self._ssh_channel = self._node.main_session.interactive_session.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.settimeout(self._timeout)
>          self._ssh_channel.set_combine_stderr(True)  # combines stdout and stderr streams
> -        self._logger = logger
> -        self._timeout = timeout
> -        self._app_params = app_params
> -        self._start_application(get_privileged_command)
>
> -    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
> +    def _make_start_command(self) -> str:
> +        """Makes the command that starts the interactive shell."""
> +        return f"{self.path} {self._app_params or ''}"
> +
> +    def start_application(self) -> None:
>          """Starts a new interactive application based on the path to the app.
>
>          This method is often overridden by subclasses as their process for
>          starting may look different.
> -
> -        Args:
> -            get_privileged_command: A function (but could be any callable) that produces
> -                the version of the command with elevated privileges.
>          """
> -        start_command = f"{self.path} {self._app_params}"
> -        if get_privileged_command is not None:
> -            start_command = get_privileged_command(start_command)
> +        self._setup_ssh_channel()
> +
> +        start_command = self._make_start_command()
> +        if self._privileged:
> +            start_command = self._node.main_session._get_privileged_command(start_command)
>          self.send_command(start_command)
>
>      def send_command(self, command: str, prompt: str | None = None) -> str:
> @@ -150,3 +160,7 @@ def close(self) -> None:
>      def __del__(self) -> None:
>          """Make sure the session is properly closed before deleting the object."""
>          self.close()
> +
> +    @classmethod
> +    def _update_path(cls, path: PurePath) -> None:
> +        cls.path = path
> diff --git a/dts/framework/remote_session/python_shell.py b/dts/framework/remote_session/python_shell.py
> index ccfd3783e8..953ed100df 100644
> --- a/dts/framework/remote_session/python_shell.py
> +++ b/dts/framework/remote_session/python_shell.py
> @@ -6,9 +6,7 @@
>  Typical usage example in a TestSuite::
>
>      from framework.remote_session import PythonShell
> -    python_shell = self.tg_node.create_interactive_shell(
> -        PythonShell, timeout=5, privileged=True
> -    )
> +    python_shell = PythonShell(self.tg_node, timeout=5, privileged=True)
>      python_shell.send_command("print('Hello World')")
>      python_shell.close()
>  """
> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
> index ef3f23c582..39985000b9 100644
> --- a/dts/framework/remote_session/testpmd_shell.py
> +++ b/dts/framework/remote_session/testpmd_shell.py
> @@ -7,9 +7,7 @@
>
>  Typical usage example in a TestSuite::
>
> -    testpmd_shell = self.sut_node.create_interactive_shell(
> -            TestPmdShell, privileged=True
> -        )
> +    testpmd_shell = TestPmdShell(self.sut_node)
>      devices = testpmd_shell.get_devices()
>      for device in devices:
>          print(device)
> @@ -18,13 +16,14 @@
>
>  import time
>  from pathlib import PurePath
> -from typing import Callable, ClassVar
> +from typing import ClassVar
>
>  from framework.exception import InteractiveCommandExecutionError
>  from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
> +from framework.remote_session.dpdk_shell import DPDKShell
>  from framework.settings import SETTINGS
> -
> -from .interactive_shell import InteractiveShell
> +from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
> +from framework.testbed_model.sut_node import SutNode
>
>
>  class TestPmdDevice(object):
> @@ -49,52 +48,48 @@ def __str__(self) -> str:
>          return self.pci_address
>
>
> -class TestPmdShell(InteractiveShell):
> +class TestPmdShell(DPDKShell):
>      """Testpmd interactive shell.
>
>      The testpmd shell users should never use
>      the :meth:`~.interactive_shell.InteractiveShell.send_command` method directly, but rather
>      call specialized methods. If there isn't one that satisfies a need, it should be added.
> -
> -    Attributes:
> -        number_of_ports: The number of ports which were allowed on the command-line when testpmd
> -            was started.
>      """
>
> -    number_of_ports: int
> +    _app_params: TestPmdParams
>
>      #: The path to the testpmd executable.
>      path: ClassVar[PurePath] = PurePath("app", "dpdk-testpmd")
>
> -    #: Flag this as a DPDK app so that it's clear this is not a system app and
> -    #: needs to be looked in a specific path.
> -    dpdk_app: ClassVar[bool] = True
> -
>      #: The testpmd's prompt.
>      _default_prompt: ClassVar[str] = "testpmd>"
>
>      #: This forces the prompt to appear after sending a command.
>      _command_extra_chars: ClassVar[str] = "\n"
>
> -    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
> -        """Overrides :meth:`~.interactive_shell._start_application`.
> -
> -        Add flags for starting testpmd in interactive mode and disabling messages for link state
> -        change events before starting the application. Link state is verified before starting
> -        packet forwarding and the messages create unexpected newlines in the terminal which
> -        complicates output collection.
> -
> -        Also find the number of pci addresses which were allowed on the command line when the app
> -        was started.
> -        """
> -        assert isinstance(self._app_params, TestPmdParams)
> -
> -        self.number_of_ports = (
> -            len(self._app_params.ports) if self._app_params.ports is not None else 0
> +    def __init__(
> +        self,
> +        node: SutNode,
> +        privileged: bool = True,
> +        timeout: float = SETTINGS.timeout,
> +        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
> +        ascending_cores: bool = True,
> +        append_prefix_timestamp: bool = True,
> +        start_on_init: bool = True,
> +        **app_params,
> +    ) -> None:
> +        """Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs."""
> +        super().__init__(
> +            node,
> +            TestPmdParams(**app_params),
> +            privileged,
> +            timeout,
> +            lcore_filter_specifier,
> +            ascending_cores,
> +            append_prefix_timestamp,
> +            start_on_init,
>          )
>
> -        super()._start_application(get_privileged_command)
> -
>      def start(self, verify: bool = True) -> None:
>          """Start packet forwarding with the current configuration.
>
> @@ -114,7 +109,8 @@ def start(self, verify: bool = True) -> None:
>                  self._logger.debug(f"Failed to start packet forwarding: \n{start_cmd_output}")
>                  raise InteractiveCommandExecutionError("Testpmd failed to start packet forwarding.")
>
> -            for port_id in range(self.number_of_ports):
> +            number_of_ports = len(self._app_params.ports or [])
> +            for port_id in range(number_of_ports):
>                  if not self.wait_link_status_up(port_id):
>                      raise InteractiveCommandExecutionError(
>                          "Not all ports came up after starting packet forwarding in testpmd."
> diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
> index 6af4f25a3c..88395faabe 100644
> --- a/dts/framework/testbed_model/node.py
> +++ b/dts/framework/testbed_model/node.py
> @@ -15,7 +15,7 @@
>
>  from abc import ABC
>  from ipaddress import IPv4Interface, IPv6Interface
> -from typing import Any, Callable, Type, Union
> +from typing import Any, Callable, Union
>
>  from framework.config import (
>      OS,
> @@ -25,7 +25,6 @@
>  )
>  from framework.exception import ConfigurationError
>  from framework.logger import DTSLogger, get_dts_logger
> -from framework.params import Params
>  from framework.settings import SETTINGS
>
>  from .cpu import (
> @@ -36,7 +35,7 @@
>      lcore_filter,
>  )
>  from .linux_session import LinuxSession
> -from .os_session import InteractiveShellType, OSSession
> +from .os_session import OSSession
>  from .port import Port
>  from .virtual_device import VirtualDevice
>
> @@ -196,37 +195,6 @@ def create_session(self, name: str) -> OSSession:
>          self._other_sessions.append(connection)
>          return connection
>
> -    def create_interactive_shell(
> -        self,
> -        shell_cls: Type[InteractiveShellType],
> -        timeout: float = SETTINGS.timeout,
> -        privileged: bool = False,
> -        app_params: Params = Params(),
> -    ) -> InteractiveShellType:
> -        """Factory for interactive session handlers.
> -
> -        Instantiate `shell_cls` according to the remote OS specifics.
> -
> -        Args:
> -            shell_cls: The class of the shell.
> -            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.
> -            privileged: Whether to run the shell with administrative privileges.
> -            app_args: The arguments to be passed to the application.
> -
> -        Returns:
> -            An instance of the desired interactive application shell.
> -        """
> -        if not shell_cls.dpdk_app:
> -            shell_cls.path = self.main_session.join_remote_path(shell_cls.path)
> -
> -        return self.main_session.create_interactive_shell(
> -            shell_cls,
> -            timeout,
> -            privileged,
> -            app_params,
> -        )
> -
>      def filter_lcores(
>          self,
>          filter_specifier: LogicalCoreCount | LogicalCoreList,
> diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
> index e5f5fcbe0e..e7e6c9d670 100644
> --- a/dts/framework/testbed_model/os_session.py
> +++ b/dts/framework/testbed_model/os_session.py
> @@ -26,18 +26,16 @@
>  from collections.abc import Iterable
>  from ipaddress import IPv4Interface, IPv6Interface
>  from pathlib import PurePath
> -from typing import Type, TypeVar, Union
> +from typing import Union
>
>  from framework.config import Architecture, NodeConfiguration, NodeInfo
>  from framework.logger import DTSLogger
> -from framework.params import Params
>  from framework.remote_session import (
>      InteractiveRemoteSession,
>      RemoteSession,
>      create_interactive_session,
>      create_remote_session,
>  )
> -from framework.remote_session.interactive_shell import InteractiveShell
>  from framework.remote_session.remote_session import CommandResult
>  from framework.settings import SETTINGS
>  from framework.utils import MesonArgs
> @@ -45,8 +43,6 @@
>  from .cpu import LogicalCore
>  from .port import Port
>
> -InteractiveShellType = TypeVar("InteractiveShellType", bound=InteractiveShell)
> -
>
>  class OSSession(ABC):
>      """OS-unaware to OS-aware translation API definition.
> @@ -131,36 +127,6 @@ def send_command(
>
>          return self.remote_session.send_command(command, timeout, verify, env)
>
> -    def create_interactive_shell(
> -        self,
> -        shell_cls: Type[InteractiveShellType],
> -        timeout: float,
> -        privileged: bool,
> -        app_args: Params,
> -    ) -> InteractiveShellType:
> -        """Factory for interactive session handlers.
> -
> -        Instantiate `shell_cls` according to the remote OS specifics.
> -
> -        Args:
> -            shell_cls: The class of the shell.
> -            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.
> -            privileged: Whether to run the shell with administrative privileges.
> -            app_args: The arguments to be passed to the application.
> -
> -        Returns:
> -            An instance of the desired interactive application shell.
> -        """
> -        return shell_cls(
> -            self.interactive_session.session,
> -            self._logger,
> -            self._get_privileged_command if privileged else None,
> -            app_args,
> -            timeout,
> -        )
> -
>      @staticmethod
>      @abstractmethod
>      def _get_privileged_command(command: str) -> str:
> diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
> index 83ad06ae2d..727170b7fc 100644
> --- a/dts/framework/testbed_model/sut_node.py
> +++ b/dts/framework/testbed_model/sut_node.py
> @@ -16,7 +16,6 @@
>  import tarfile
>  import time
>  from pathlib import PurePath
> -from typing import Type
>
>  from framework.config import (
>      BuildTargetConfiguration,
> @@ -24,17 +23,13 @@
>      NodeInfo,
>      SutNodeConfiguration,
>  )
> -from framework.params import Params, Switch
>  from framework.params.eal import EalParams
>  from framework.remote_session.remote_session import CommandResult
>  from framework.settings import SETTINGS
>  from framework.utils import MesonArgs
>
> -from .cpu import LogicalCoreCount, LogicalCoreList
>  from .node import Node
> -from .os_session import InteractiveShellType, OSSession
> -from .port import Port
> -from .virtual_device import VirtualDevice
> +from .os_session import OSSession
>
>
>  class SutNode(Node):
> @@ -289,68 +284,6 @@ def kill_cleanup_dpdk_apps(self) -> None:
>              self._dpdk_kill_session = self.create_session("dpdk_kill")
>          self._dpdk_prefix_list = []
>
> -    def create_eal_parameters(
> -        self,
> -        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
> -        ascending_cores: bool = True,
> -        prefix: str = "dpdk",
> -        append_prefix_timestamp: bool = True,
> -        no_pci: Switch = None,
> -        vdevs: list[VirtualDevice] | None = None,
> -        ports: list[Port] | None = None,
> -        other_eal_param: str = "",
> -    ) -> EalParams:
> -        """Compose the EAL parameters.
> -
> -        Process the list of cores and the DPDK prefix and pass that along with
> -        the rest of the arguments.
> -
> -        Args:
> -            lcore_filter_specifier: A number of lcores/cores/sockets to use
> -                or a list of lcore ids to use.
> -                The default will select one lcore for each of two cores
> -                on one socket, in ascending order of core ids.
> -            ascending_cores: Sort cores in ascending order (lowest to highest IDs).
> -                If :data:`False`, sort in descending order.
> -            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
> -            append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.
> -            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
> -            vdevs: Virtual devices, e.g.::
> -
> -                vdevs=[
> -                    VirtualDevice('net_ring0'),
> -                    VirtualDevice('net_ring1')
> -                ]
> -            ports: The list of ports to allow. If :data:`None`, all ports listed in `self.ports`
> -                will be allowed.
> -            other_eal_param: user defined DPDK EAL parameters, e.g.:
> -                ``other_eal_param='--single-file-segments'``.
> -
> -        Returns:
> -            An EAL param string, such as
> -            ``-c 0xf -a 0000:88:00.0 --file-prefix=dpdk_1112_20190809143420``.
> -        """
> -        lcore_list = LogicalCoreList(self.filter_lcores(lcore_filter_specifier, ascending_cores))
> -
> -        if append_prefix_timestamp:
> -            prefix = f"{prefix}_{self._dpdk_timestamp}"
> -        prefix = self.main_session.get_dpdk_file_prefix(prefix)
> -        if prefix:
> -            self._dpdk_prefix_list.append(prefix)
> -
> -        if ports is None:
> -            ports = self.ports
> -
> -        return EalParams(
> -            lcore_list=lcore_list,
> -            memory_channels=self.config.memory_channels,
> -            prefix=prefix,
> -            no_pci=no_pci,
> -            vdevs=vdevs,
> -            ports=ports,
> -            other_eal_param=Params.from_str(other_eal_param),
> -        )
> -
>      def run_dpdk_app(
>          self, app_path: PurePath, eal_params: EalParams, timeout: float = 30
>      ) -> CommandResult:
> @@ -379,49 +312,6 @@ def configure_ipv4_forwarding(self, enable: bool) -> None:
>          """
>          self.main_session.configure_ipv4_forwarding(enable)
>
> -    def create_interactive_shell(
> -        self,
> -        shell_cls: Type[InteractiveShellType],
> -        timeout: float = SETTINGS.timeout,
> -        privileged: bool = False,
> -        app_params: Params = Params(),
> -        eal_params: EalParams | None = None,
> -    ) -> InteractiveShellType:
> -        """Extend the factory for interactive session handlers.
> -
> -        The extensions are SUT node specific:
> -
> -            * The default for `eal_parameters`,
> -            * The interactive shell path `shell_cls.path` is prepended with path to the remote
> -              DPDK build directory for DPDK apps.
> -
> -        Args:
> -            shell_cls: The class of the shell.
> -            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.
> -            privileged: Whether to run the shell with administrative privileges.
> -            app_params: The parameters to be passed to the application.
> -            eal_params: List of EAL parameters to use to launch the app. If this
> -                isn't provided or an empty string is passed, it will default to calling
> -                :meth:`create_eal_parameters`.
> -
> -        Returns:
> -            An instance of the desired interactive application shell.
> -        """
> -        # We need to append the build directory and add EAL parameters for DPDK apps
> -        if shell_cls.dpdk_app:
> -            if eal_params is None:
> -                eal_params = self.create_eal_parameters()
> -            eal_params.append_str(str(app_params))
> -            app_params = eal_params
> -
> -            shell_cls.path = self.main_session.join_remote_path(
> -                self.remote_dpdk_build_dir, shell_cls.path
> -            )
> -
> -        return super().create_interactive_shell(shell_cls, timeout, privileged, app_params)
> -
>      def bind_ports_to_driver(self, for_dpdk: bool = True) -> None:
>          """Bind all ports on the SUT to a driver.
>
> diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py
> index 7bc1c2cc08..bf58ad1c5e 100644
> --- a/dts/framework/testbed_model/traffic_generator/scapy.py
> +++ b/dts/framework/testbed_model/traffic_generator/scapy.py
> @@ -217,9 +217,7 @@ def __init__(self, tg_node: Node, config: ScapyTrafficGeneratorConfig):
>              self._tg_node.config.os == OS.linux
>          ), "Linux is the only supported OS for scapy traffic generation"
>
> -        self.session = self._tg_node.create_interactive_shell(
> -            PythonShell, timeout=5, privileged=True
> -        )
> +        self.session = PythonShell(self._tg_node, timeout=5, privileged=True)
>
>          # import libs in remote python console
>          for import_statement in SCAPY_RPC_SERVER_IMPORTS:
> diff --git a/dts/tests/TestSuite_hello_world.py b/dts/tests/TestSuite_hello_world.py
> index 0d6995f260..d958f99030 100644
> --- a/dts/tests/TestSuite_hello_world.py
> +++ b/dts/tests/TestSuite_hello_world.py
> @@ -7,6 +7,7 @@
>  No other EAL parameters apart from cores are used.
>  """
>
> +from framework.remote_session.dpdk_shell import compute_eal_params
>  from framework.test_suite import TestSuite
>  from framework.testbed_model.cpu import (
>      LogicalCoreCount,
> @@ -38,7 +39,7 @@ def test_hello_world_single_core(self) -> None:
>          # get the first usable core
>          lcore_amount = LogicalCoreCount(1, 1, 1)
>          lcores = LogicalCoreCountFilter(self.sut_node.lcores, lcore_amount).filter()
> -        eal_para = self.sut_node.create_eal_parameters(lcore_filter_specifier=lcore_amount)
> +        eal_para = compute_eal_params(self.sut_node, lcore_filter_specifier=lcore_amount)
>          result = self.sut_node.run_dpdk_app(self.app_helloworld_path, eal_para)
>          self.verify(
>              f"hello from core {int(lcores[0])}" in result.stdout,
> @@ -55,8 +56,8 @@ def test_hello_world_all_cores(self) -> None:
>              "hello from core <core_id>"
>          """
>          # get the maximum logical core number
> -        eal_para = self.sut_node.create_eal_parameters(
> -            lcore_filter_specifier=LogicalCoreList(self.sut_node.lcores)
> +        eal_para = compute_eal_params(
> +            self.sut_node, lcore_filter_specifier=LogicalCoreList(self.sut_node.lcores)
>          )
>          result = self.sut_node.run_dpdk_app(self.app_helloworld_path, eal_para, 50)
>          for lcore in self.sut_node.lcores:
> diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
> index 6d206c1a40..43cf5c61eb 100644
> --- a/dts/tests/TestSuite_pmd_buffer_scatter.py
> +++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
> @@ -16,14 +16,13 @@
>  """
>
>  import struct
> -from dataclasses import asdict
>
>  from scapy.layers.inet import IP  # type: ignore[import-untyped]
>  from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
>  from scapy.packet import Raw  # type: ignore[import-untyped]
>  from scapy.utils import hexstr  # type: ignore[import-untyped]
>
> -from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
> +from framework.params.testpmd import SimpleForwardingModes
>  from framework.remote_session.testpmd_shell import TestPmdShell
>  from framework.test_suite import TestSuite
>
> @@ -103,17 +102,13 @@ def pmd_scatter(self, mbsize: int) -> None:
>          Test:
>              Start testpmd and run functional test with preset mbsize.
>          """
> -        testpmd = self.sut_node.create_interactive_shell(
> -            TestPmdShell,
> -            app_params=TestPmdParams(
> -                forward_mode=SimpleForwardingModes.mac,
> -                mbcache=200,
> -                mbuf_size=[mbsize],
> -                max_pkt_len=9000,
> -                tx_offloads=0x00008000,
> -                **asdict(self.sut_node.create_eal_parameters()),
> -            ),
> -            privileged=True,
> +        testpmd = TestPmdShell(
> +            self.sut_node,
> +            forward_mode=SimpleForwardingModes.mac,
> +            mbcache=200,
> +            mbuf_size=[mbsize],
> +            max_pkt_len=9000,
> +            tx_offloads=0x00008000,
>          )
>          testpmd.start()
>
> diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py
> index ca678f662d..eca27acfd8 100644
> --- a/dts/tests/TestSuite_smoke_tests.py
> +++ b/dts/tests/TestSuite_smoke_tests.py
> @@ -99,7 +99,7 @@ def test_devices_listed_in_testpmd(self) -> None:
>          Test:
>              List all devices found in testpmd and verify the configured devices are among them.
>          """
> -        testpmd_driver = self.sut_node.create_interactive_shell(TestPmdShell, privileged=True)
> +        testpmd_driver = TestPmdShell(self.sut_node)
>          dev_list = [str(x) for x in testpmd_driver.get_devices()]
>          for nic in self.nics_in_node:
>              self.verify(
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 6/8] dts: use testpmd params for scatter test suite
  2024-05-30 15:25   ` [PATCH v3 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
  2024-05-30 20:13     ` Jeremy Spewock
@ 2024-05-31 15:22     ` Nicholas Pratte
  2024-06-06 14:38     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-05-31 15:22 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Juraj Linkeš, Jeremy Spewock, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Thu, May 30, 2024 at 11:25 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Update the buffer scatter test suite to use TestPmdParameters
> instead of the StrParams implementation.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>  dts/tests/TestSuite_pmd_buffer_scatter.py | 18 +++++++++---------
>  1 file changed, 9 insertions(+), 9 deletions(-)
>
> diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
> index 578b5a4318..6d206c1a40 100644
> --- a/dts/tests/TestSuite_pmd_buffer_scatter.py
> +++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
> @@ -16,14 +16,14 @@
>  """
>
>  import struct
> +from dataclasses import asdict
>
>  from scapy.layers.inet import IP  # type: ignore[import-untyped]
>  from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
>  from scapy.packet import Raw  # type: ignore[import-untyped]
>  from scapy.utils import hexstr  # type: ignore[import-untyped]
>
> -from framework.params import Params
> -from framework.params.testpmd import SimpleForwardingModes
> +from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
>  from framework.remote_session.testpmd_shell import TestPmdShell
>  from framework.test_suite import TestSuite
>
> @@ -105,16 +105,16 @@ def pmd_scatter(self, mbsize: int) -> None:
>          """
>          testpmd = self.sut_node.create_interactive_shell(
>              TestPmdShell,
> -            app_params=Params.from_str(
> -                "--mbcache=200 "
> -                f"--mbuf-size={mbsize} "
> -                "--max-pkt-len=9000 "
> -                "--port-topology=paired "
> -                "--tx-offloads=0x00008000"
> +            app_params=TestPmdParams(
> +                forward_mode=SimpleForwardingModes.mac,
> +                mbcache=200,
> +                mbuf_size=[mbsize],
> +                max_pkt_len=9000,
> +                tx_offloads=0x00008000,
> +                **asdict(self.sut_node.create_eal_parameters()),
>              ),
>              privileged=True,
>          )
> -        testpmd.set_forward_mode(SimpleForwardingModes.mac)
>          testpmd.start()
>
>          for offset in [-1, 0, 1, 4, 5]:
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 1/8] dts: add params manipulation module
  2024-05-09 11:20   ` [PATCH v2 1/8] dts: add params manipulation module Luca Vizzarro
  2024-05-28 15:40     ` Nicholas Pratte
  2024-05-28 21:08     ` Jeremy Spewock
@ 2024-06-06  9:19     ` Juraj Linkeš
  2024-06-17 11:44       ` Luca Vizzarro
  2 siblings, 1 reply; 159+ messages in thread
From: Juraj Linkeš @ 2024-06-06  9:19 UTC (permalink / raw)
  To: Luca Vizzarro, dev; +Cc: Jeremy Spewock, Paul Szczepanek

The docstrings are missing the Args: or Returns: sections.

On 9. 5. 2024 13:20, Luca Vizzarro wrote:
> This commit introduces a new "params" module, which adds a new way
> to manage command line parameters. The provided Params dataclass
> is able to read the fields of its child class and produce a string
> representation to supply to the command line. Any data structure
> that is intended to represent command line parameters can inherit it.
> 
> The main purpose is to make it easier to represent data structures that
> map to parameters. Aiding quicker development, while minimising code
> bloat.
> 
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>   dts/framework/params/__init__.py | 274 +++++++++++++++++++++++++++++++
>   1 file changed, 274 insertions(+)
>   create mode 100644 dts/framework/params/__init__.py
> 
> diff --git a/dts/framework/params/__init__.py b/dts/framework/params/__init__.py
> new file mode 100644
> index 0000000000..aa27e34357
> --- /dev/null
> +++ b/dts/framework/params/__init__.py
> @@ -0,0 +1,274 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2024 Arm Limited
> +
> +"""Parameter manipulation module.
> +
> +This module provides :class:`Params` which can be used to model any data structure
> +that is meant to represent any command parameters.
> +"""

This should probably end with command line parameters.

> +
> +from dataclasses import dataclass, fields
> +from enum import Flag
> +from typing import Any, Callable, Iterable, Literal, Reversible, TypedDict, cast
> +
> +from typing_extensions import Self
> +
> +#: Type for a function taking one argument.
> +FnPtr = Callable[[Any], Any]
> +#: Type for a switch parameter.
> +Switch = Literal[True, None]
> +#: Type for a yes/no switch parameter.
> +YesNoSwitch = Literal[True, False, None]
> +
> +
> +def _reduce_functions(funcs: Reversible[FnPtr]) -> FnPtr:

The static method in the other patch is called compose and does 
essentially the same thing, right? Can we use the same name (or a 
similar one)?

Also, what is the difference in approaches between the two patches (or, 
more accurately, the reason behind the difference)? In the other patch, 
we're returning a dict, here we're returning a function directly.

> +    """Reduces an iterable of :attr:`FnPtr` from end to start to a composite function.

We should make the order of application the same as in the method in 
other patch, so if we change the order in the first one, we should do 
the same here.

> +
> +    If the iterable is empty, the created function just returns its fed value back.
> +    """
> +
> +    def composite_function(value: Any):

The return type is missing.

> +        for fn in reversed(funcs):
> +            value = fn(value)
> +        return value
> +
> +    return composite_function
> +
> +
> +def convert_str(*funcs: FnPtr):

The return type is missing.

And maybe the name could be better, now it suggests to me that we're 
converting the __str__ method, but we're actually replacing it, so maybe 
replace_str() or modify_str()?

> +    """Decorator that makes the ``__str__`` method a composite function created from its arguments.

This should mention that it's a class decorator (and that it replaces or 
modifies the __str__() method).

> +
> +    The :attr:`FnPtr`s fed to the decorator are executed from right to left
> +    in the arguments list order.
> +
> +    Example:
> +    .. code:: python
> +
> +        @convert_str(hex_from_flag_value)
> +        class BitMask(enum.Flag):
> +            A = auto()
> +            B = auto()
> +
> +    will allow ``BitMask`` to render as a hexadecimal value.
> +    """
> +
> +    def _class_decorator(original_class):
> +        original_class.__str__ = _reduce_functions(funcs)
> +        return original_class
> +
> +    return _class_decorator
> +
> +
> +def comma_separated(values: Iterable[Any]) -> str:
> +    """Converts an iterable in a comma-separated string."""
> +    return ",".join([str(value).strip() for value in values if value is not None])
> +
> +
> +def bracketed(value: str) -> str:
> +    """Adds round brackets to the input."""
> +    return f"({value})"
> +
> +
> +def str_from_flag_value(flag: Flag) -> str:
> +    """Returns the value from a :class:`enum.Flag` as a string."""
> +    return str(flag.value)
> +
> +
> +def hex_from_flag_value(flag: Flag) -> str:
> +    """Returns the value from a :class:`enum.Flag` converted to hexadecimal."""
> +    return hex(flag.value)
> +
> +
> +class ParamsModifier(TypedDict, total=False):
> +    """Params modifiers dict compatible with the :func:`dataclasses.field` metadata parameter."""
> +
> +    #:
> +    Params_value_only: bool
> +    #:
> +    Params_short: str
> +    #:
> +    Params_long: str
> +    #:
> +    Params_multiple: bool
> +    #:
> +    Params_convert_value: Reversible[FnPtr]
> +
> +
> +@dataclass
> +class Params:
> +    """Dataclass that renders its fields into command line arguments.
> +
> +    The parameter name is taken from the field name by default. The following:
> +
> +    .. code:: python
> +
> +        name: str | None = "value"
> +
> +    is rendered as ``--name=value``.
> +    Through :func:`dataclasses.field` the resulting parameter can be manipulated by applying
> +    this class' metadata modifier functions.
> +
> +    To use fields as switches, set the value to ``True`` to render them. If you
> +    use a yes/no switch you can also set ``False`` which would render a switch
> +    prefixed with ``--no-``. Examples:
> +
> +    .. code:: python
> +
> +        interactive: Switch = True  # renders --interactive
> +        numa: YesNoSwitch   = False # renders --no-numa
> +
> +    Setting ``None`` will prevent it from being rendered. The :attr:`~Switch` type alias is provided
> +    for regular switches, whereas :attr:`~YesNoSwitch` is offered for yes/no ones.
> +
> +    An instance of a dataclass inheriting ``Params`` can also be assigned to an attribute,
> +    this helps with grouping parameters together.
> +    The attribute holding the dataclass will be ignored and the latter will just be rendered as
> +    expected.
> +    """
> +
> +    _suffix = ""
> +    """Holder of the plain text value of Params when called directly. A suffix for child classes."""
> +
> +    """========= BEGIN FIELD METADATA MODIFIER FUNCTIONS ========"""
> +
> +    @staticmethod
> +    def value_only() -> ParamsModifier:

As far as I (or my IDE) can tell, this is not used anywhere. What's the 
purpose of this?

> +        """Injects the value of the attribute as-is without flag.
> +
> +        Metadata modifier for :func:`dataclasses.field`.
> +        """
> +        return ParamsModifier(Params_value_only=True)
> +
> +    @staticmethod
> +    def short(name: str) -> ParamsModifier:
> +        """Overrides any parameter name with the given short option.
> +
> +        Metadata modifier for :func:`dataclasses.field`.
> +
> +        Example:
> +        .. code:: python
> +
> +            logical_cores: str | None = field(default="1-4", metadata=Params.short("l"))
> +
> +        will render as ``-l=1-4`` instead of ``--logical-cores=1-4``.
> +        """
> +        return ParamsModifier(Params_short=name)
> +
> +    @staticmethod
> +    def long(name: str) -> ParamsModifier:
> +        """Overrides the inferred parameter name to the specified one.
> +
> +        Metadata modifier for :func:`dataclasses.field`.
> +
> +        Example:
> +        .. code:: python
> +
> +            x_name: str | None = field(default="y", metadata=Params.long("x"))
> +
> +        will render as ``--x=y``, but the field is accessed and modified through ``x_name``.
> +        """
> +        return ParamsModifier(Params_long=name)
> +
> +    @staticmethod
> +    def multiple() -> ParamsModifier:
> +        """Specifies that this parameter is set multiple times. Must be a list.
> +
> +        Metadata modifier for :func:`dataclasses.field`.
> +
> +        Example:
> +        .. code:: python
> +
> +            ports: list[int] | None = field(
> +                default_factory=lambda: [0, 1, 2],
> +                metadata=Params.multiple() | Params.long("port")
> +            )
> +
> +        will render as ``--port=0 --port=1 --port=2``. Note that modifiers can be chained like
> +        in this example.

I'd put the explanation of how modifiers can be chained (and mention 
they're dicts so the or operator just merges the dicts) into the class 
docstring. Then we won't have to duplicate the explanation in each method.

> +        """
> +        return ParamsModifier(Params_multiple=True)
> +
> +    @classmethod
> +    def convert_value(cls, *funcs: FnPtr) -> ParamsModifier:

I don't see cls used anywhere, so let's make this static.

> +        """Takes in a variable number of functions to convert the value text representation.
> +
> +        Metadata modifier for :func:`dataclasses.field`.
> +
> +        The ``metadata`` keyword argument can be used to chain metadata modifiers together.
> +
> +        Functions can be chained together, executed from right to left in the arguments list order.
> +
> +        Example:
> +        .. code:: python
> +
> +            hex_bitmask: int | None = field(
> +                default=0b1101,
> +                metadata=Params.convert_value(hex) | Params.long("mask")
> +            )
> +
> +        will render as ``--mask=0xd``.
> +        """
> +        return ParamsModifier(Params_convert_value=funcs)
> +
> +    """========= END FIELD METADATA MODIFIER FUNCTIONS ========"""
> +
> +    def append_str(self, text: str) -> None:
> +        """Appends a string at the end of the string representation."""
> +        self._suffix += text
> +
> +    def __iadd__(self, text: str) -> Self:
> +        """Appends a string at the end of the string representation."""
> +        self.append_str(text)
> +        return self
> +
> +    @classmethod
> +    def from_str(cls, text: str) -> Self:

I tried to figure out how self._suffix is used and I ended up finding 
out this method is not used anywhere. Is that correct? If it's not used, 
let's remove it.

What actually should be the suffix? A an arbitrary string that gets 
appended to the rendered command line arguments? I guess this would be 
here so that we can pass an already rendered string?

> +        """Creates a plain Params object from a string."""
> +        obj = cls()
> +        obj.append_str(text)
> +        return obj
> +
> +    @staticmethod
> +    def _make_switch(
> +        name: str, is_short: bool = False, is_no: bool = False, value: str | None = None
> +    ) -> str:
> +        prefix = f"{'-' if is_short else '--'}{'no-' if is_no else ''}"

Does is_short work with is_no (that is, if both are True)? Do we need to 
worry about it if not?

> +        name = name.replace("_", "-")
> +        value = f"{' ' if is_short else '='}{value}" if value else ""
> +        return f"{prefix}{name}{value}"
> +
> +    def __str__(self) -> str:
> +        """Returns a string of command-line-ready arguments from the class fields."""
> +        arguments: list[str] = []
> +
> +        for field in fields(self):
> +            value = getattr(self, field.name)
> +            modifiers = cast(ParamsModifier, field.metadata)
> +
> +            if value is None:
> +                continue
> +
> +            value_only = modifiers.get("Params_value_only", False)
> +            if isinstance(value, Params) or value_only:
> +                arguments.append(str(value))
> +                continue
> +
> +            # take the short modifier, or the long modifier, or infer from field name
> +            switch_name = modifiers.get("Params_short", modifiers.get("Params_long", field.name))
> +            is_short = "Params_short" in modifiers
> +
> +            if isinstance(value, bool):
> +                arguments.append(self._make_switch(switch_name, is_short, is_no=(not value)))
> +                continue
> +
> +            convert = _reduce_functions(modifiers.get("Params_convert_value", []))
> +            multiple = modifiers.get("Params_multiple", False)
> +
> +            values = value if multiple else [value]
> +            for value in values:
> +                arguments.append(self._make_switch(switch_name, is_short, value=convert(value)))
> +
> +        if self._suffix:
> +            arguments.append(self._suffix)
> +
> +        return " ".join(arguments)

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 2/8] dts: use Params for interactive shells
  2024-05-09 11:20   ` [PATCH v2 2/8] dts: use Params for interactive shells Luca Vizzarro
  2024-05-28 17:43     ` Nicholas Pratte
  2024-05-28 21:04     ` Jeremy Spewock
@ 2024-06-06 13:14     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Juraj Linkeš @ 2024-06-06 13:14 UTC (permalink / raw)
  To: Luca Vizzarro, dev; +Cc: Jeremy Spewock, Paul Szczepanek


On 9. 5. 2024 13:20, Luca Vizzarro wrote:
> Make it so that interactive shells accept an implementation of `Params`
> for app arguments. Convert EalParameters to use `Params` instead.
> 
> String command line parameters can still be supplied by using the
> `Params.from_str()` method.
> 
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>

Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>

> diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
> index 97aa26d419..c886590979 100644
> --- a/dts/framework/testbed_model/sut_node.py
> +++ b/dts/framework/testbed_model/sut_node.py

> @@ -393,24 +376,21 @@ def create_eal_parameters(
>           if prefix:
>               self._dpdk_prefix_list.append(prefix)
>   
> -        if vdevs is None:
> -            vdevs = []
> -
>           if ports is None:
>               ports = self.ports
>   
> -        return EalParameters(
> +        return EalParams(
>               lcore_list=lcore_list,
>               memory_channels=self.config.memory_channels,
>               prefix=prefix,
>               no_pci=no_pci,
>               vdevs=vdevs,
>               ports=ports,
> -            other_eal_param=other_eal_param,
> +            other_eal_param=Params.from_str(other_eal_param),

So this is where from_str() is used. I guess it didn't get removed in a 
subsequent patche where the last usage of it was removed.

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 3/8] dts: refactor EalParams
  2024-05-09 11:20   ` [PATCH v2 3/8] dts: refactor EalParams Luca Vizzarro
  2024-05-28 15:44     ` Nicholas Pratte
  2024-05-28 21:05     ` Jeremy Spewock
@ 2024-06-06 13:17     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Juraj Linkeš @ 2024-06-06 13:17 UTC (permalink / raw)
  To: Luca Vizzarro, dev; +Cc: Jeremy Spewock, Paul Szczepanek



On 9. 5. 2024 13:20, Luca Vizzarro wrote:
> Move EalParams to its own module to avoid circular dependencies.
> 

Maybe the commit message could mention that we added defaults.

> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>

Other than that,
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 4/8] dts: remove module-wide imports
  2024-05-09 11:20   ` [PATCH v2 4/8] dts: remove module-wide imports Luca Vizzarro
  2024-05-28 15:45     ` Nicholas Pratte
  2024-05-28 21:08     ` Jeremy Spewock
@ 2024-06-06 13:21     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Juraj Linkeš @ 2024-06-06 13:21 UTC (permalink / raw)
  To: Luca Vizzarro, dev; +Cc: Jeremy Spewock, Paul Szczepanek



On 9. 5. 2024 13:20, Luca Vizzarro wrote:
> Remove the imports in the testbed_model and remote_session modules init
> file, to avoid the initialisation of unneeded modules, thus removing or
> limiting the risk of circular dependencies.
> 
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>   dts/framework/remote_session/__init__.py               | 5 +----
>   dts/framework/runner.py                                | 4 +++-
>   dts/framework/test_suite.py                            | 5 ++++-
>   dts/framework/testbed_model/__init__.py                | 7 -------
>   dts/framework/testbed_model/os_session.py              | 4 ++--
>   dts/framework/testbed_model/sut_node.py                | 2 +-
>   dts/framework/testbed_model/traffic_generator/scapy.py | 2 +-
>   dts/tests/TestSuite_hello_world.py                     | 2 +-
>   dts/tests/TestSuite_smoke_tests.py                     | 2 +-
>   9 files changed, 14 insertions(+), 19 deletions(-)
> 

> diff --git a/dts/framework/testbed_model/__init__.py b/dts/framework/testbed_model/__init__.py
> index 6086512ca2..4f8a58c039 100644
> --- a/dts/framework/testbed_model/__init__.py
> +++ b/dts/framework/testbed_model/__init__.py
> @@ -19,10 +19,3 @@
>   """
>   
>   # pylama:ignore=W0611

There's no reason to leave this in now. It may not be needed in 
dts/framework/remote_session/__init__.py as well.

> -
> -from .cpu import LogicalCoreCount, LogicalCoreCountFilter, LogicalCoreList
> -from .node import Node
> -from .port import Port, PortLink
> -from .sut_node import SutNode
> -from .tg_node import TGNode
> -from .virtual_device import VirtualDevice

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 5/8] dts: add testpmd shell params
  2024-05-30 15:25   ` [PATCH v3 5/8] dts: add testpmd shell params Luca Vizzarro
  2024-05-30 20:12     ` Jeremy Spewock
  2024-05-31 15:20     ` Nicholas Pratte
@ 2024-06-06 14:37     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Juraj Linkeš @ 2024-06-06 14:37 UTC (permalink / raw)
  To: Luca Vizzarro, dev; +Cc: Jeremy Spewock, Paul Szczepanek

Apparently I was replying to v2 so fixing that from now on.

On 30. 5. 2024 17:25, Luca Vizzarro wrote:
> Implement all the testpmd shell parameters into a data structure.
> 
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>

Just one minor correction, otherwise:
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>

> +@dataclass(slots=True, kw_only=True)
> +class TestPmdParams(EalParams):
> +    """The testpmd shell parameters.
> +
> +    Attributes:
> +        interactive_mode: Runs testpmd in interactive mode.

"Run testpmd" to unify with the rest of the attributes.

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 6/8] dts: use testpmd params for scatter test suite
  2024-05-30 15:25   ` [PATCH v3 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
  2024-05-30 20:13     ` Jeremy Spewock
  2024-05-31 15:22     ` Nicholas Pratte
@ 2024-06-06 14:38     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Juraj Linkeš @ 2024-06-06 14:38 UTC (permalink / raw)
  To: Luca Vizzarro, dev; +Cc: Jeremy Spewock, Paul Szczepanek



On 30. 5. 2024 17:25, Luca Vizzarro wrote:
> Update the buffer scatter test suite to use TestPmdParameters
> instead of the StrParams implementation.
> 
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 7/8] dts: rework interactive shells
  2024-05-30 15:25   ` [PATCH v3 7/8] dts: rework interactive shells Luca Vizzarro
  2024-05-30 20:13     ` Jeremy Spewock
  2024-05-31 15:22     ` Nicholas Pratte
@ 2024-06-06 18:03     ` Juraj Linkeš
  2024-06-17 12:13       ` Luca Vizzarro
  2 siblings, 1 reply; 159+ messages in thread
From: Juraj Linkeš @ 2024-06-06 18:03 UTC (permalink / raw)
  To: Luca Vizzarro, dev; +Cc: Jeremy Spewock, Paul Szczepanek


> diff --git a/dts/framework/remote_session/dpdk_shell.py b/dts/framework/remote_session/dpdk_shell.py
> new file mode 100644
> index 0000000000..25e3df4eaa
> --- /dev/null
> +++ b/dts/framework/remote_session/dpdk_shell.py
> @@ -0,0 +1,104 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2024 Arm Limited
> +
> +"""DPDK-based interactive shell.

I think this means the shell uses DPDK libraries. This would be better 
worded as "Base interactive shell for DPDK applications."

> +
> +Provides a base class to create interactive shells based on DPDK.
> +"""
> +
> +
> +from abc import ABC
> +
> +from framework.params.eal import EalParams
> +from framework.remote_session.interactive_shell import InteractiveShell
> +from framework.settings import SETTINGS
> +from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
> +from framework.testbed_model.sut_node import SutNode
> +
> +
> +def compute_eal_params(
> +    node: SutNode,

Let's rename this sut_node. I got confused a bit when reading the code.

> +    params: EalParams | None = None,
> +    lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
> +    ascending_cores: bool = True,
> +    append_prefix_timestamp: bool = True,
> +) -> EalParams:
> +    """Compute EAL parameters based on the node's specifications.
> +
> +    Args:
> +        node: The SUT node to compute the values for.
> +        params: The EalParams object to amend, if set to None a new object is created and returned.

This could use some additional explanation about how it's amended - 
what's replaced, what isn't and in general what happens.

> +        lcore_filter_specifier: A number of lcores/cores/sockets to use
> +            or a list of lcore ids to use.
> +            The default will select one lcore for each of two cores
> +            on one socket, in ascending order of core ids.
> +        ascending_cores: Sort cores in ascending order (lowest to highest IDs).
> +            If :data:`False`, sort in descending order.
> +        append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.
> +    """
> +    if params is None:
> +        params = EalParams()
> +
> +    if params.lcore_list is None:
> +        params.lcore_list = LogicalCoreList(
> +            node.filter_lcores(lcore_filter_specifier, ascending_cores)
> +        )
> +
> +    prefix = params.prefix
> +    if append_prefix_timestamp:
> +        prefix = f"{prefix}_{node._dpdk_timestamp}"
> +    prefix = node.main_session.get_dpdk_file_prefix(prefix)
> +    if prefix:
> +        node._dpdk_prefix_list.append(prefix)

We should make _dpdk_prefix_list public. Also _dpdk_timestamp.

> +    params.prefix = prefix
> +
> +    if params.ports is None:
> +        params.ports = node.ports
> +
> +    return params
> +
> +
> +class DPDKShell(InteractiveShell, ABC):
> +    """The base class for managing DPDK-based interactive shells.
> +
> +    This class shouldn't be instantiated directly, but instead be extended.
> +    It automatically injects computed EAL parameters based on the node in the
> +    supplied app parameters.
> +    """
> +
> +    _node: SutNode

Same here, better to be explicit with _sut_node.

> +    _app_params: EalParams
> +
> +    _lcore_filter_specifier: LogicalCoreCount | LogicalCoreList
> +    _ascending_cores: bool
> +    _append_prefix_timestamp: bool
> +
> +    def __init__(
> +        self,
> +        node: SutNode,
> +        app_params: EalParams,
> +        privileged: bool = True,
> +        timeout: float = SETTINGS.timeout,
> +        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
> +        ascending_cores: bool = True,
> +        append_prefix_timestamp: bool = True,
> +        start_on_init: bool = True,
> +    ) -> None:
> +        """Overrides :meth:`~.interactive_shell.InteractiveShell.__init__`."""
> +        self._lcore_filter_specifier = lcore_filter_specifier
> +        self._ascending_cores = ascending_cores
> +        self._append_prefix_timestamp = append_prefix_timestamp
> +
> +        super().__init__(node, app_params, privileged, timeout, start_on_init)
> +
> +    def _post_init(self):
> +        """Computes EAL params based on the node capabilities before start."""

We could just put this before calling super().__init__() in this class 
if we update path some other way, right? It's probably better to 
override the class method (_update_path()) in subclasses than having 
this _post_init() method.

> +        self._app_params = compute_eal_params(
> +            self._node,
> +            self._app_params,
> +            self._lcore_filter_specifier,
> +            self._ascending_cores,
> +            self._append_prefix_timestamp,
> +        )
> +
> +        self._update_path(self._node.remote_dpdk_build_dir.joinpath(self.path))
> diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
> index 9da66d1c7e..4be7966672 100644
> --- a/dts/framework/remote_session/interactive_shell.py
> +++ b/dts/framework/remote_session/interactive_shell.py

> @@ -56,55 +58,63 @@ class InteractiveShell(ABC):
<snip>
> +    def start_application(self) -> None:
>           """Starts a new interactive application based on the path to the app.
>   
>           This method is often overridden by subclasses as their process for
>           starting may look different.
> -
> -        Args:
> -            get_privileged_command: A function (but could be any callable) that produces
> -                the version of the command with elevated privileges.
>           """
> -        start_command = f"{self.path} {self._app_params}"
> -        if get_privileged_command is not None:
> -            start_command = get_privileged_command(start_command)
> +        self._setup_ssh_channel()
> +
> +        start_command = self._make_start_command()
> +        if self._privileged:
> +            start_command = self._node.main_session._get_privileged_command(start_command)

This update of the command should be in _make_start_command().

>           self.send_command(start_command)
>   
>       def send_command(self, command: str, prompt: str | None = None) -> str:

> @@ -49,52 +48,48 @@ def __str__(self) -> str:
<snip>
> +    def __init__(
> +        self,
> +        node: SutNode,
> +        privileged: bool = True,
> +        timeout: float = SETTINGS.timeout,
> +        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
> +        ascending_cores: bool = True,
> +        append_prefix_timestamp: bool = True,
> +        start_on_init: bool = True,
> +        **app_params,
> +    ) -> None:
> +        """Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs."""
> +        super().__init__(
> +            node,
> +            TestPmdParams(**app_params),
> +            privileged,
> +            timeout,
> +            lcore_filter_specifier,
> +            ascending_cores,
> +            append_prefix_timestamp,
> +            start_on_init,

Just a note on the differences in signatures. TestPmdShell has the 
parameters at the end while DPDKShell and InteractiveShell have them 
second. I think we could make app_params the last parameter in all of 
these classes - that works for both kwargs and just singular Params.

>           )
>   
> -        super()._start_application(get_privileged_command)
> -
>       def start(self, verify: bool = True) -> None:
>           """Start packet forwarding with the current configuration.
>   

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 8/8] dts: use Unpack for type checking and hinting
  2024-05-30 15:25   ` [PATCH v3 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
  2024-05-30 20:13     ` Jeremy Spewock
  2024-05-31 15:21     ` Nicholas Pratte
@ 2024-06-06 18:05     ` Juraj Linkeš
  2 siblings, 0 replies; 159+ messages in thread
From: Juraj Linkeš @ 2024-06-06 18:05 UTC (permalink / raw)
  To: Luca Vizzarro, dev; +Cc: Jeremy Spewock, Paul Szczepanek



On 30. 5. 2024 17:25, Luca Vizzarro wrote:
> Interactive shells that inherit DPDKShell initialise their params
> classes from a kwargs dict. Therefore, static type checking is
> disabled. This change uses the functionality of Unpack added in
> PEP 692 to re-enable it. The disadvantage is that this functionality has
> been implemented only with TypedDict, forcing the creation of TypedDict
> mirrors of the Params classes.
> 
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>

Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 1/8] dts: add params manipulation module
  2024-06-06  9:19     ` Juraj Linkeš
@ 2024-06-17 11:44       ` Luca Vizzarro
  2024-06-18  8:55         ` Juraj Linkeš
  0 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-17 11:44 UTC (permalink / raw)
  To: Juraj Linkeš, dev; +Cc: Jeremy Spewock, Paul Szczepanek

On 06/06/2024 10:19, Juraj Linkeš wrote:
> The static method in the other patch is called compose and does 
> essentially the same thing, right? Can we use the same name (or a 
> similar one)?
> 
> Also, what is the difference in approaches between the two patches (or, 
> more accurately, the reason behind the difference)? In the other patch, 
> we're returning a dict, here we're returning a function directly.

They are essentially different. This one is as it's called quite plainly 
a function reduction. Can be used in any context in reality.

The other one is tighter and has some specific controls (exit early if 
None), and is directly represented as a dictionary as that's the only 
intended way of consumption.

>> +    """Reduces an iterable of :attr:`FnPtr` from end to start to a 
>> composite function.
> 
> We should make the order of application the same as in the method in 
> other patch, so if we change the order in the first one, we should do 
> the same here.

While I don't think it feels any natural coding-wise (yet again, a 
matter of common readability to the developer vs how it's actually run), 
I won't object as I don't have a preference.

>> +
>> +    If the iterable is empty, the created function just returns its 
>> fed value back.
>> +    """
>> +
>> +    def composite_function(value: Any):
> 
> The return type is missing.

I will remove types from the decorator functions as it adds too much 
complexity and little advantage. I wasn't able to easily resolve with 
mypy. Especially in conjunction with modify_str, where mypy complains a 
lot about __str__ being treated as an unbound method/plain function that 
doesn't take self.

>> +    _suffix = ""
>> +    """Holder of the plain text value of Params when called directly. 
>> A suffix for child classes."""
>> +
>> +    """========= BEGIN FIELD METADATA MODIFIER FUNCTIONS ========"""
>> +
>> +    @staticmethod
>> +    def value_only() -> ParamsModifier:
> 
> As far as I (or my IDE) can tell, this is not used anywhere. What's the 
> purpose of this?

I guess this is no longer in use at the moment. It could still be used 
for positional arguments in the future. But will remove for now.

>> +    def append_str(self, text: str) -> None:
>> +        """Appends a string at the end of the string representation."""
>> +        self._suffix += text
>> +
>> +    def __iadd__(self, text: str) -> Self:
>> +        """Appends a string at the end of the string representation."""
>> +        self.append_str(text)
>> +        return self
>> +
>> +    @classmethod
>> +    def from_str(cls, text: str) -> Self:
> 
> I tried to figure out how self._suffix is used and I ended up finding 
> out this method is not used anywhere. Is that correct? If it's not used, 
> let's remove it.

It is used through Params.from_str. It is used transitionally in the 
next commits.

> What actually should be the suffix? A an arbitrary string that gets 
> appended to the rendered command line arguments? I guess this would be 
> here so that we can pass an already rendered string?

As we already discussed and agreed before, this is just to use Params 
plainly with arbitrary text. It is treated as a suffix, because if 
Params is inherited this stays, and then it can either be a prefix or a 
suffix in that case.

>> +        """Creates a plain Params object from a string."""
>> +        obj = cls()
>> +        obj.append_str(text)
>> +        return obj
>> +
>> +    @staticmethod
>> +    def _make_switch(
>> +        name: str, is_short: bool = False, is_no: bool = False, 
>> value: str | None = None
>> +    ) -> str:
>> +        prefix = f"{'-' if is_short else '--'}{'no-' if is_no else ''}"
> 
> Does is_short work with is_no (that is, if both are True)? Do we need to 
> worry about it if not?

They should not work together, and no it is not enforced. But finally 
that's up to the command line interface we are modelling the parameters 
against. It is not a problem in reality.


^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 7/8] dts: rework interactive shells
  2024-06-06 18:03     ` Juraj Linkeš
@ 2024-06-17 12:13       ` Luca Vizzarro
  2024-06-18  9:18         ` Juraj Linkeš
  0 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-17 12:13 UTC (permalink / raw)
  To: Juraj Linkeš, dev; +Cc: Jeremy Spewock, Paul Szczepanek

On 06/06/2024 19:03, Juraj Linkeš wrote:
>> +class DPDKShell(InteractiveShell, ABC):
>> +    """The base class for managing DPDK-based interactive shells.
>> +
>> +    This class shouldn't be instantiated directly, but instead be 
>> extended.
>> +    It automatically injects computed EAL parameters based on the 
>> node in the
>> +    supplied app parameters.
>> +    """
>> +
>> +    _node: SutNode
> 
> Same here, better to be explicit with _sut_node.

This should not be changed as it's just overriding the type of the 
parent's attribute.

>> +    _app_params: EalParams
>> +
>> +    _lcore_filter_specifier: LogicalCoreCount | LogicalCoreList
>> +    _ascending_cores: bool
>> +    _append_prefix_timestamp: bool
>> +
>> +    def __init__(
>> +        self,
>> +        node: SutNode,
>> +        app_params: EalParams,
>> +        privileged: bool = True,
>> +        timeout: float = SETTINGS.timeout,
>> +        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = 
>> LogicalCoreCount(),
>> +        ascending_cores: bool = True,
>> +        append_prefix_timestamp: bool = True,
>> +        start_on_init: bool = True,
>> +    ) -> None:
>> +        """Overrides 
>> :meth:`~.interactive_shell.InteractiveShell.__init__`."""
>> +        self._lcore_filter_specifier = lcore_filter_specifier
>> +        self._ascending_cores = ascending_cores
>> +        self._append_prefix_timestamp = append_prefix_timestamp
>> +
>> +        super().__init__(node, app_params, privileged, timeout, 
>> start_on_init)
>> +
>> +    def _post_init(self):
>> +        """Computes EAL params based on the node capabilities before 
>> start."""
> 
> We could just put this before calling super().__init__() in this class 
> if we update path some other way, right? It's probably better to 
> override the class method (_update_path()) in subclasses than having 
> this _post_init() method.

It's more complicated than this. The ultimate parent (InteractiveShell) 
is what sets all the common attributes. This needs to happen before 
compute_eal_params and updating the path with the dpdk folder. This 
wouldn't be a problem if we were to call super().__init__ at the 
beginning. But that way we'd lose the ability to automatically start the 
shell though.

^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v4 0/8] dts: add testpmd params and statefulness
  2024-03-26 19:04 [PATCH 0/6] dts: add testpmd params and statefulness Luca Vizzarro
                   ` (7 preceding siblings ...)
  2024-05-30 15:24 ` [PATCH v3 " Luca Vizzarro
@ 2024-06-17 14:42 ` Luca Vizzarro
  2024-06-17 14:42   ` [PATCH v4 1/8] dts: add params manipulation module Luca Vizzarro
                     ` (7 more replies)
  2024-06-17 14:54 ` [PATCH v5 0/8] dts: add testpmd params Luca Vizzarro
                   ` (2 subsequent siblings)
  11 siblings, 8 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-17 14:42 UTC (permalink / raw)
  To: dev; +Cc: Jeremy Spewock, Juraj Linkeš, Luca Vizzarro

v4:
- fixed up docstrings
- made refactoring changes
- removed params value only
- rebased on top of show port info/stats
v3:
- refactored InteractiveShell methods
- fixed docstrings
v2:
- refactored the params module
- strengthened typing of the params module
- moved the params module into its own package
- refactored EalParams and TestPmdParams and
  moved under the params package
- reworked interactions between nodes and shells
- refactored imports leading to circular dependencies

---
Depends-on: series-32112 ("dts: testpmd show port info/stats")
---

Luca Vizzarro (8):
  dts: add params manipulation module
  dts: use Params for interactive shells
  dts: refactor EalParams
  dts: remove module-wide imports
  dts: add testpmd shell params
  dts: use testpmd params for scatter test suite
  dts: rework interactive shells
  dts: use Unpack for type checking and hinting

 dts/framework/params/__init__.py              | 358 +++++++++++
 dts/framework/params/eal.py                   |  50 ++
 dts/framework/params/testpmd.py               | 607 ++++++++++++++++++
 dts/framework/params/types.py                 | 133 ++++
 dts/framework/remote_session/__init__.py      |   7 +-
 dts/framework/remote_session/dpdk_shell.py    | 106 +++
 .../remote_session/interactive_shell.py       |  83 ++-
 dts/framework/remote_session/python_shell.py  |   4 +-
 dts/framework/remote_session/testpmd_shell.py |  99 +--
 dts/framework/runner.py                       |   4 +-
 dts/framework/test_suite.py                   |   5 +-
 dts/framework/testbed_model/__init__.py       |   9 -
 dts/framework/testbed_model/node.py           |  36 +-
 dts/framework/testbed_model/os_session.py     |  38 +-
 dts/framework/testbed_model/sut_node.py       | 194 +-----
 .../testbed_model/traffic_generator/scapy.py  |   6 +-
 dts/tests/TestSuite_hello_world.py            |   9 +-
 dts/tests/TestSuite_pmd_buffer_scatter.py     |  21 +-
 dts/tests/TestSuite_smoke_tests.py            |   4 +-
 19 files changed, 1384 insertions(+), 389 deletions(-)
 create mode 100644 dts/framework/params/__init__.py
 create mode 100644 dts/framework/params/eal.py
 create mode 100644 dts/framework/params/testpmd.py
 create mode 100644 dts/framework/params/types.py
 create mode 100644 dts/framework/remote_session/dpdk_shell.py

-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v4 1/8] dts: add params manipulation module
  2024-06-17 14:42 ` [PATCH v4 0/8] dts: add testpmd params and statefulness Luca Vizzarro
@ 2024-06-17 14:42   ` Luca Vizzarro
  2024-06-17 14:42   ` [PATCH v4 2/8] dts: use Params for interactive shells Luca Vizzarro
                     ` (6 subsequent siblings)
  7 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-17 14:42 UTC (permalink / raw)
  To: dev; +Cc: Jeremy Spewock, Juraj Linkeš, Luca Vizzarro, Paul Szczepanek

This commit introduces a new "params" module, which adds a new way
to manage command line parameters. The provided Params dataclass
is able to read the fields of its child class and produce a string
representation to supply to the command line. Any data structure
that is intended to represent command line parameters can inherit it.

The main purpose is to make it easier to represent data structures that
map to parameters. Aiding quicker development, while minimising code
bloat.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 dts/framework/params/__init__.py | 358 +++++++++++++++++++++++++++++++
 1 file changed, 358 insertions(+)
 create mode 100644 dts/framework/params/__init__.py

diff --git a/dts/framework/params/__init__.py b/dts/framework/params/__init__.py
new file mode 100644
index 0000000000..1203b8ce98
--- /dev/null
+++ b/dts/framework/params/__init__.py
@@ -0,0 +1,358 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Parameter manipulation module.
+
+This module provides :class:`Params` which can be used to model any data structure
+that is meant to represent any command line parameters.
+"""
+
+from dataclasses import dataclass, fields
+from enum import Flag
+from typing import (
+    Any,
+    Callable,
+    Iterable,
+    Literal,
+    Reversible,
+    TypedDict,
+    TypeVar,
+    cast,
+)
+
+from typing_extensions import Self
+
+T = TypeVar("T")
+
+#: Type for a function taking one argument.
+FnPtr = Callable[[Any], Any]
+#: Type for a switch parameter.
+Switch = Literal[True, None]
+#: Type for a yes/no switch parameter.
+YesNoSwitch = Literal[True, False, None]
+
+
+def _reduce_functions(funcs: Iterable[FnPtr]) -> FnPtr:
+    """Reduces an iterable of :attr:`FnPtr` from left to right to a single function.
+
+    If the iterable is empty, the created function just returns its fed value back.
+
+    Args:
+        funcs: An iterable containing the functions to be chained from left to right.
+
+    Returns:
+        FnPtr: A function that calls the given functions from left to right.
+    """
+
+    def reduced_fn(value):
+        for fn in funcs:
+            value = fn(value)
+        return value
+
+    return reduced_fn
+
+
+def modify_str(*funcs: FnPtr) -> Callable[[T], T]:
+    """Class decorator modifying the ``__str__`` method with a function created from its arguments.
+
+    The :attr:`FnPtr`s fed to the decorator are executed from left to right in the arguments list
+    order.
+
+    Args:
+        *funcs: The functions to chain from left to right.
+
+    Returns:
+        The decorator.
+
+    Example:
+        .. code:: python
+
+            @convert_str(hex_from_flag_value)
+            class BitMask(enum.Flag):
+                A = auto()
+                B = auto()
+
+        will allow ``BitMask`` to render as a hexadecimal value.
+    """
+
+    def _class_decorator(original_class):
+        original_class.__str__ = _reduce_functions(funcs)
+        return original_class
+
+    return _class_decorator
+
+
+def comma_separated(values: Iterable[Any]) -> str:
+    """Converts an iterable into a comma-separated string.
+
+    Args:
+        values: An iterable of objects.
+
+    Returns:
+        A comma-separated list of stringified values.
+    """
+    return ",".join([str(value).strip() for value in values if value is not None])
+
+
+def bracketed(value: str) -> str:
+    """Adds round brackets to the input.
+
+    Args:
+        value: Any string.
+
+    Returns:
+        A string surrounded by round brackets.
+    """
+    return f"({value})"
+
+
+def str_from_flag_value(flag: Flag) -> str:
+    """Returns the value from a :class:`enum.Flag` as a string.
+
+    Args:
+        flag: An instance of :class:`Flag`.
+
+    Returns:
+        The stringified value of the given flag.
+    """
+    return str(flag.value)
+
+
+def hex_from_flag_value(flag: Flag) -> str:
+    """Returns the value from a :class:`enum.Flag` converted to hexadecimal.
+
+    Args:
+        flag: An instance of :class:`Flag`.
+
+    Returns:
+        The value of the given flag in hexadecimal representation.
+    """
+    return hex(flag.value)
+
+
+class ParamsModifier(TypedDict, total=False):
+    """Params modifiers dict compatible with the :func:`dataclasses.field` metadata parameter."""
+
+    #:
+    Params_short: str
+    #:
+    Params_long: str
+    #:
+    Params_multiple: bool
+    #:
+    Params_convert_value: Reversible[FnPtr]
+
+
+@dataclass
+class Params:
+    """Dataclass that renders its fields into command line arguments.
+
+    The parameter name is taken from the field name by default. The following:
+
+    .. code:: python
+
+        name: str | None = "value"
+
+    is rendered as ``--name=value``.
+    Through :func:`dataclasses.field` the resulting parameter can be manipulated by applying
+    this class' metadata modifier functions. These return regular dictionaries which can be combined
+    together using the pipe (OR) operator.
+
+    To use fields as switches, set the value to ``True`` to render them. If you
+    use a yes/no switch you can also set ``False`` which would render a switch
+    prefixed with ``--no-``. Examples:
+
+    .. code:: python
+
+        interactive: Switch = True  # renders --interactive
+        numa: YesNoSwitch   = False # renders --no-numa
+
+    Setting ``None`` will prevent it from being rendered. The :attr:`~Switch` type alias is provided
+    for regular switches, whereas :attr:`~YesNoSwitch` is offered for yes/no ones.
+
+    An instance of a dataclass inheriting ``Params`` can also be assigned to an attribute,
+    this helps with grouping parameters together.
+    The attribute holding the dataclass will be ignored and the latter will just be rendered as
+    expected.
+    """
+
+    _suffix = ""
+    """Holder of the plain text value of Params when called directly. A suffix for child classes."""
+
+    """========= BEGIN FIELD METADATA MODIFIER FUNCTIONS ========"""
+
+    @staticmethod
+    def short(name: str) -> ParamsModifier:
+        """Overrides any parameter name with the given short option.
+
+        Args:
+            name: The short parameter name.
+
+        Returns:
+            ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
+                the parameter short name modifier.
+
+        Example:
+            .. code:: python
+
+                logical_cores: str | None = field(default="1-4", metadata=Params.short("l"))
+
+            will render as ``-l=1-4`` instead of ``--logical-cores=1-4``.
+        """
+        return ParamsModifier(Params_short=name)
+
+    @staticmethod
+    def long(name: str) -> ParamsModifier:
+        """Overrides the inferred parameter name to the specified one.
+
+        Args:
+            name: The long parameter name.
+
+        Returns:
+            ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
+                the parameter long name modifier.
+
+        Example:
+            .. code:: python
+
+                x_name: str | None = field(default="y", metadata=Params.long("x"))
+
+            will render as ``--x=y``, but the field is accessed and modified through ``x_name``.
+        """
+        return ParamsModifier(Params_long=name)
+
+    @staticmethod
+    def multiple() -> ParamsModifier:
+        """Specifies that this parameter is set multiple times. The parameter type must be a list.
+
+        Returns:
+            ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
+                the multiple parameters modifier.
+
+        Example:
+            .. code:: python
+
+                ports: list[int] | None = field(
+                    default_factory=lambda: [0, 1, 2],
+                    metadata=Params.multiple() | Params.long("port")
+                )
+
+            will render as ``--port=0 --port=1 --port=2``.
+        """
+        return ParamsModifier(Params_multiple=True)
+
+    @staticmethod
+    def convert_value(*funcs: FnPtr) -> ParamsModifier:
+        """Takes in a variable number of functions to convert the value text representation.
+
+        Functions can be chained together, executed from left to right in the arguments list order.
+
+        Args:
+            *funcs: The functions to chain from left to right.
+
+        Returns:
+            ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
+                the convert value modifier.
+
+        Example:
+            .. code:: python
+
+                hex_bitmask: int | None = field(
+                    default=0b1101,
+                    metadata=Params.convert_value(hex) | Params.long("mask")
+                )
+
+            will render as ``--mask=0xd``.
+        """
+        return ParamsModifier(Params_convert_value=funcs)
+
+    """========= END FIELD METADATA MODIFIER FUNCTIONS ========"""
+
+    def append_str(self, text: str) -> None:
+        """Appends a string at the end of the string representation.
+
+        Args:
+            text: Any text to append at the end of the parameters string representation.
+        """
+        self._suffix += text
+
+    def __iadd__(self, text: str) -> Self:
+        """Appends a string at the end of the string representation.
+
+        Args:
+            text: Any text to append at the end of the parameters string representation.
+
+        Returns:
+            The given instance back.
+        """
+        self.append_str(text)
+        return self
+
+    @classmethod
+    def from_str(cls, text: str) -> Self:
+        """Creates a plain Params object from a string.
+
+        Args:
+            text: The string parameters.
+
+        Returns:
+            A new plain instance of :class:`Params`.
+        """
+        obj = cls()
+        obj.append_str(text)
+        return obj
+
+    @staticmethod
+    def _make_switch(
+        name: str, is_short: bool = False, is_no: bool = False, value: str | None = None
+    ) -> str:
+        """Make the string representation of the parameter.
+
+        Args:
+            name: The name of the parameters.
+            is_short: If the paremeters is short or not.
+            is_no: If the parameter is negated or not.
+            value: The value of the parameter.
+
+        Returns:
+            The complete command line parameter.
+        """
+        prefix = f"{'-' if is_short else '--'}{'no-' if is_no else ''}"
+        name = name.replace("_", "-")
+        value = f"{' ' if is_short else '='}{value}" if value else ""
+        return f"{prefix}{name}{value}"
+
+    def __str__(self) -> str:
+        """Returns a string of command-line-ready arguments from the class fields."""
+        arguments: list[str] = []
+
+        for field in fields(self):
+            value = getattr(self, field.name)
+            modifiers = cast(ParamsModifier, field.metadata)
+
+            if value is None:
+                continue
+
+            if isinstance(value, Params):
+                arguments.append(str(value))
+                continue
+
+            # take the short modifier, or the long modifier, or infer from field name
+            switch_name = modifiers.get("Params_short", modifiers.get("Params_long", field.name))
+            is_short = "Params_short" in modifiers
+
+            if isinstance(value, bool):
+                arguments.append(self._make_switch(switch_name, is_short, is_no=(not value)))
+                continue
+
+            convert = _reduce_functions(modifiers.get("Params_convert_value", []))
+            multiple = modifiers.get("Params_multiple", False)
+
+            values = value if multiple else [value]
+            for value in values:
+                arguments.append(self._make_switch(switch_name, is_short, value=convert(value)))
+
+        if self._suffix:
+            arguments.append(self._suffix)
+
+        return " ".join(arguments)
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v4 2/8] dts: use Params for interactive shells
  2024-06-17 14:42 ` [PATCH v4 0/8] dts: add testpmd params and statefulness Luca Vizzarro
  2024-06-17 14:42   ` [PATCH v4 1/8] dts: add params manipulation module Luca Vizzarro
@ 2024-06-17 14:42   ` Luca Vizzarro
  2024-06-17 14:42   ` [PATCH v4 3/8] dts: refactor EalParams Luca Vizzarro
                     ` (5 subsequent siblings)
  7 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-17 14:42 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Make it so that interactive shells accept an implementation of `Params`
for app arguments. Convert EalParameters to use `Params` instead.

String command line parameters can still be supplied by using the
`Params.from_str()` method.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 .../remote_session/interactive_shell.py       |  12 +-
 dts/framework/remote_session/testpmd_shell.py |  11 +-
 dts/framework/testbed_model/node.py           |   6 +-
 dts/framework/testbed_model/os_session.py     |   4 +-
 dts/framework/testbed_model/sut_node.py       | 124 ++++++++----------
 dts/tests/TestSuite_pmd_buffer_scatter.py     |   3 +-
 6 files changed, 77 insertions(+), 83 deletions(-)

diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
index c025c52ba3..8191b36630 100644
--- a/dts/framework/remote_session/interactive_shell.py
+++ b/dts/framework/remote_session/interactive_shell.py
@@ -1,5 +1,6 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright(c) 2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """Common functionality for interactive shell handling.
 
@@ -21,6 +22,7 @@
 from paramiko import Channel, SSHClient, channel  # type: ignore[import-untyped]
 
 from framework.logger import DTSLogger
+from framework.params import Params
 from framework.settings import SETTINGS
 
 
@@ -40,7 +42,7 @@ class InteractiveShell(ABC):
     _ssh_channel: Channel
     _logger: DTSLogger
     _timeout: float
-    _app_args: str
+    _app_params: Params
 
     #: Prompt to expect at the end of output when sending a command.
     #: This is often overridden by subclasses.
@@ -63,7 +65,7 @@ def __init__(
         interactive_session: SSHClient,
         logger: DTSLogger,
         get_privileged_command: Callable[[str], str] | None,
-        app_args: str = "",
+        app_params: Params = Params(),
         timeout: float = SETTINGS.timeout,
     ) -> None:
         """Create an SSH channel during initialization.
@@ -74,7 +76,7 @@ def __init__(
             get_privileged_command: A method for modifying a command to allow it to use
                 elevated privileges. If :data:`None`, the application will not be started
                 with elevated privileges.
-            app_args: The command line arguments to be passed to the application on startup.
+            app_params: The command line parameters to be passed to the application on startup.
             timeout: The timeout used for the SSH channel that is dedicated to this interactive
                 shell. This timeout is for collecting output, so if reading from the buffer
                 and no output is gathered within the timeout, an exception is thrown.
@@ -87,7 +89,7 @@ def __init__(
         self._ssh_channel.set_combine_stderr(True)  # combines stdout and stderr streams
         self._logger = logger
         self._timeout = timeout
-        self._app_args = app_args
+        self._app_params = app_params
         self._start_application(get_privileged_command)
 
     def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
@@ -100,7 +102,7 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
             get_privileged_command: A function (but could be any callable) that produces
                 the version of the command with elevated privileges.
         """
-        start_command = f"{self.path} {self._app_args}"
+        start_command = f"{self.path} {self._app_params}"
         if get_privileged_command is not None:
             start_command = get_privileged_command(start_command)
         self.send_command(start_command)
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index d413bf2cc7..2836ed5c48 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -28,6 +28,7 @@
 from framework.exception import InteractiveCommandExecutionError
 from framework.parser import ParserFn, TextParser
 from framework.settings import SETTINGS
+from framework.testbed_model.sut_node import EalParams
 from framework.utils import StrEnum
 
 from .interactive_shell import InteractiveShell
@@ -645,8 +646,14 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
         Also find the number of pci addresses which were allowed on the command line when the app
         was started.
         """
-        self._app_args += " -i --mask-event intr_lsc"
-        self.number_of_ports = self._app_args.count("-a ")
+        self._app_params += " -i --mask-event intr_lsc"
+
+        assert isinstance(self._app_params, EalParams)
+
+        self.number_of_ports = (
+            len(self._app_params.ports) if self._app_params.ports is not None else 0
+        )
+
         super()._start_application(get_privileged_command)
 
     def start(self, verify: bool = True) -> None:
diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
index 74061f6262..6af4f25a3c 100644
--- a/dts/framework/testbed_model/node.py
+++ b/dts/framework/testbed_model/node.py
@@ -2,6 +2,7 @@
 # Copyright(c) 2010-2014 Intel Corporation
 # Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2022-2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """Common functionality for node management.
 
@@ -24,6 +25,7 @@
 )
 from framework.exception import ConfigurationError
 from framework.logger import DTSLogger, get_dts_logger
+from framework.params import Params
 from framework.settings import SETTINGS
 
 from .cpu import (
@@ -199,7 +201,7 @@ def create_interactive_shell(
         shell_cls: Type[InteractiveShellType],
         timeout: float = SETTINGS.timeout,
         privileged: bool = False,
-        app_args: str = "",
+        app_params: Params = Params(),
     ) -> InteractiveShellType:
         """Factory for interactive session handlers.
 
@@ -222,7 +224,7 @@ def create_interactive_shell(
             shell_cls,
             timeout,
             privileged,
-            app_args,
+            app_params,
         )
 
     def filter_lcores(
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
index d5bf7e0401..1a77aee532 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright(c) 2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """OS-aware remote session.
 
@@ -29,6 +30,7 @@
 
 from framework.config import Architecture, NodeConfiguration, NodeInfo
 from framework.logger import DTSLogger
+from framework.params import Params
 from framework.remote_session import (
     CommandResult,
     InteractiveRemoteSession,
@@ -134,7 +136,7 @@ def create_interactive_shell(
         shell_cls: Type[InteractiveShellType],
         timeout: float,
         privileged: bool,
-        app_args: str,
+        app_args: Params,
     ) -> InteractiveShellType:
         """Factory for interactive session handlers.
 
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index 97aa26d419..c886590979 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -2,6 +2,7 @@
 # Copyright(c) 2010-2014 Intel Corporation
 # Copyright(c) 2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """System under test (DPDK + hardware) node.
 
@@ -14,8 +15,9 @@
 import os
 import tarfile
 import time
+from dataclasses import dataclass, field
 from pathlib import PurePath
-from typing import Type
+from typing import Literal, Type
 
 from framework.config import (
     BuildTargetConfiguration,
@@ -23,6 +25,7 @@
     NodeInfo,
     SutNodeConfiguration,
 )
+from framework.params import Params, Switch
 from framework.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
@@ -34,62 +37,42 @@
 from .virtual_device import VirtualDevice
 
 
-class EalParameters(object):
-    """The environment abstraction layer parameters.
-
-    The string representation can be created by converting the instance to a string.
-    """
+def _port_to_pci(port: Port) -> str:
+    return port.pci
 
-    def __init__(
-        self,
-        lcore_list: LogicalCoreList,
-        memory_channels: int,
-        prefix: str,
-        no_pci: bool,
-        vdevs: list[VirtualDevice],
-        ports: list[Port],
-        other_eal_param: str,
-    ):
-        """Initialize the parameters according to inputs.
-
-        Process the parameters into the format used on the command line.
 
-        Args:
-            lcore_list: The list of logical cores to use.
-            memory_channels: The number of memory channels to use.
-            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
-            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
-            vdevs: Virtual devices, e.g.::
+@dataclass(kw_only=True)
+class EalParams(Params):
+    """The environment abstraction layer parameters.
 
-                vdevs=[
-                    VirtualDevice('net_ring0'),
-                    VirtualDevice('net_ring1')
-                ]
-            ports: The list of ports to allow.
-            other_eal_param: user defined DPDK EAL parameters, e.g.:
+    Attributes:
+        lcore_list: The list of logical cores to use.
+        memory_channels: The number of memory channels to use.
+        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
+        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
+        vdevs: Virtual devices, e.g.::
+            vdevs=[
+                VirtualDevice('net_ring0'),
+                VirtualDevice('net_ring1')
+            ]
+        ports: The list of ports to allow.
+        other_eal_param: user defined DPDK EAL parameters, e.g.:
                 ``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._ports = " ".join(f"-a {port.pci}" for port in ports)
-        self._other_eal_param = other_eal_param
-
-    def __str__(self) -> str:
-        """Create the EAL string."""
-        return (
-            f"{self._lcore_list} "
-            f"{self._memory_channels} "
-            f"{self._prefix} "
-            f"{self._no_pci} "
-            f"{self._vdevs} "
-            f"{self._ports} "
-            f"{self._other_eal_param}"
-        )
+    """
+
+    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
+    memory_channels: int = field(metadata=Params.short("n"))
+    prefix: str = field(metadata=Params.long("file-prefix"))
+    no_pci: Switch
+    vdevs: list[VirtualDevice] | None = field(
+        default=None, metadata=Params.multiple() | Params.long("vdev")
+    )
+    ports: list[Port] | None = field(
+        default=None,
+        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
+    )
+    other_eal_param: Params | None = None
+    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
 
 
 class SutNode(Node):
@@ -350,11 +333,11 @@ def create_eal_parameters(
         ascending_cores: bool = True,
         prefix: str = "dpdk",
         append_prefix_timestamp: bool = True,
-        no_pci: bool = False,
+        no_pci: Switch = None,
         vdevs: list[VirtualDevice] | None = None,
         ports: list[Port] | None = None,
         other_eal_param: str = "",
-    ) -> "EalParameters":
+    ) -> EalParams:
         """Compose the EAL parameters.
 
         Process the list of cores and the DPDK prefix and pass that along with
@@ -393,24 +376,21 @@ def create_eal_parameters(
         if prefix:
             self._dpdk_prefix_list.append(prefix)
 
-        if vdevs is None:
-            vdevs = []
-
         if ports is None:
             ports = self.ports
 
-        return EalParameters(
+        return EalParams(
             lcore_list=lcore_list,
             memory_channels=self.config.memory_channels,
             prefix=prefix,
             no_pci=no_pci,
             vdevs=vdevs,
             ports=ports,
-            other_eal_param=other_eal_param,
+            other_eal_param=Params.from_str(other_eal_param),
         )
 
     def run_dpdk_app(
-        self, app_path: PurePath, eal_args: "EalParameters", timeout: float = 30
+        self, app_path: PurePath, eal_params: EalParams, timeout: float = 30
     ) -> CommandResult:
         """Run DPDK application on the remote node.
 
@@ -419,14 +399,14 @@ def run_dpdk_app(
 
         Args:
             app_path: The remote path to the DPDK application.
-            eal_args: EAL parameters to run the DPDK application with.
+            eal_params: EAL parameters to run the DPDK application with.
             timeout: Wait at most this long in seconds for `command` execution to complete.
 
         Returns:
             The result of the DPDK app execution.
         """
         return self.main_session.send_command(
-            f"{app_path} {eal_args}", timeout, privileged=True, verify=True
+            f"{app_path} {eal_params}", timeout, privileged=True, verify=True
         )
 
     def configure_ipv4_forwarding(self, enable: bool) -> None:
@@ -442,8 +422,8 @@ def create_interactive_shell(
         shell_cls: Type[InteractiveShellType],
         timeout: float = SETTINGS.timeout,
         privileged: bool = False,
-        app_parameters: str = "",
-        eal_parameters: EalParameters | None = None,
+        app_params: Params = Params(),
+        eal_params: EalParams | None = None,
     ) -> InteractiveShellType:
         """Extend the factory for interactive session handlers.
 
@@ -459,26 +439,26 @@ def create_interactive_shell(
                 reading from the buffer and don't receive any data within the timeout
                 it will throw an error.
             privileged: Whether to run the shell with administrative privileges.
-            eal_parameters: List of EAL parameters to use to launch the app. If this
+            app_params: The parameters to be passed to the application.
+            eal_params: List of EAL parameters to use to launch the app. If this
                 isn't provided or an empty string is passed, it will default to calling
                 :meth:`create_eal_parameters`.
-            app_parameters: Additional arguments to pass into the application on the
-                command-line.
 
         Returns:
             An instance of the desired interactive application shell.
         """
         # We need to append the build directory and add EAL parameters for DPDK apps
         if shell_cls.dpdk_app:
-            if not eal_parameters:
-                eal_parameters = self.create_eal_parameters()
-            app_parameters = f"{eal_parameters} -- {app_parameters}"
+            if eal_params is None:
+                eal_params = self.create_eal_parameters()
+            eal_params.append_str(str(app_params))
+            app_params = eal_params
 
             shell_cls.path = self.main_session.join_remote_path(
                 self.remote_dpdk_build_dir, shell_cls.path
             )
 
-        return super().create_interactive_shell(shell_cls, timeout, privileged, app_parameters)
+        return super().create_interactive_shell(shell_cls, timeout, privileged, app_params)
 
     def bind_ports_to_driver(self, for_dpdk: bool = True) -> None:
         """Bind all ports on the SUT to a driver.
diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index a020682e8d..c6e93839cb 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -22,6 +22,7 @@
 from scapy.packet import Raw  # type: ignore[import-untyped]
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
+from framework.params import Params
 from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
 from framework.test_suite import TestSuite
 
@@ -103,7 +104,7 @@ def pmd_scatter(self, mbsize: int) -> None:
         """
         testpmd = self.sut_node.create_interactive_shell(
             TestPmdShell,
-            app_parameters=(
+            app_params=Params.from_str(
                 "--mbcache=200 "
                 f"--mbuf-size={mbsize} "
                 "--max-pkt-len=9000 "
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v4 3/8] dts: refactor EalParams
  2024-06-17 14:42 ` [PATCH v4 0/8] dts: add testpmd params and statefulness Luca Vizzarro
  2024-06-17 14:42   ` [PATCH v4 1/8] dts: add params manipulation module Luca Vizzarro
  2024-06-17 14:42   ` [PATCH v4 2/8] dts: use Params for interactive shells Luca Vizzarro
@ 2024-06-17 14:42   ` Luca Vizzarro
  2024-06-17 14:42   ` [PATCH v4 4/8] dts: remove module-wide imports Luca Vizzarro
                     ` (4 subsequent siblings)
  7 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-17 14:42 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Move EalParams to its own module to avoid circular dependencies.
Also the majority of the attributes are now optional.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 dts/framework/params/eal.py                   | 50 +++++++++++++++++++
 dts/framework/remote_session/testpmd_shell.py |  2 +-
 dts/framework/testbed_model/sut_node.py       | 42 +---------------
 3 files changed, 53 insertions(+), 41 deletions(-)
 create mode 100644 dts/framework/params/eal.py

diff --git a/dts/framework/params/eal.py b/dts/framework/params/eal.py
new file mode 100644
index 0000000000..bbdbc8f334
--- /dev/null
+++ b/dts/framework/params/eal.py
@@ -0,0 +1,50 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Module representing the DPDK EAL-related parameters."""
+
+from dataclasses import dataclass, field
+from typing import Literal
+
+from framework.params import Params, Switch
+from framework.testbed_model.cpu import LogicalCoreList
+from framework.testbed_model.port import Port
+from framework.testbed_model.virtual_device import VirtualDevice
+
+
+def _port_to_pci(port: Port) -> str:
+    return port.pci
+
+
+@dataclass(kw_only=True)
+class EalParams(Params):
+    """The environment abstraction layer parameters.
+
+    Attributes:
+        lcore_list: The list of logical cores to use.
+        memory_channels: The number of memory channels to use.
+        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
+        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
+        vdevs: Virtual devices, e.g.::
+            vdevs=[
+                VirtualDevice('net_ring0'),
+                VirtualDevice('net_ring1')
+            ]
+        ports: The list of ports to allow.
+        other_eal_param: user defined DPDK EAL parameters, e.g.:
+                ``other_eal_param='--single-file-segments'``
+    """
+
+    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
+    memory_channels: int = field(metadata=Params.short("n"))
+    prefix: str = field(metadata=Params.long("file-prefix"))
+    no_pci: Switch = None
+    vdevs: list[VirtualDevice] | None = field(
+        default=None, metadata=Params.multiple() | Params.long("vdev")
+    )
+    ports: list[Port] | None = field(
+        default=None,
+        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
+    )
+    other_eal_param: Params | None = None
+    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index 2836ed5c48..2b9ef9418d 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -26,9 +26,9 @@
 from typing_extensions import Self
 
 from framework.exception import InteractiveCommandExecutionError
+from framework.params.eal import EalParams
 from framework.parser import ParserFn, TextParser
 from framework.settings import SETTINGS
-from framework.testbed_model.sut_node import EalParams
 from framework.utils import StrEnum
 
 from .interactive_shell import InteractiveShell
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index c886590979..e1163106a3 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -15,9 +15,8 @@
 import os
 import tarfile
 import time
-from dataclasses import dataclass, field
 from pathlib import PurePath
-from typing import Literal, Type
+from typing import Type
 
 from framework.config import (
     BuildTargetConfiguration,
@@ -26,6 +25,7 @@
     SutNodeConfiguration,
 )
 from framework.params import Params, Switch
+from framework.params.eal import EalParams
 from framework.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
@@ -37,44 +37,6 @@
 from .virtual_device import VirtualDevice
 
 
-def _port_to_pci(port: Port) -> str:
-    return port.pci
-
-
-@dataclass(kw_only=True)
-class EalParams(Params):
-    """The environment abstraction layer parameters.
-
-    Attributes:
-        lcore_list: The list of logical cores to use.
-        memory_channels: The number of memory channels to use.
-        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
-        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
-        vdevs: Virtual devices, e.g.::
-            vdevs=[
-                VirtualDevice('net_ring0'),
-                VirtualDevice('net_ring1')
-            ]
-        ports: The list of ports to allow.
-        other_eal_param: user defined DPDK EAL parameters, e.g.:
-                ``other_eal_param='--single-file-segments'``
-    """
-
-    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
-    memory_channels: int = field(metadata=Params.short("n"))
-    prefix: str = field(metadata=Params.long("file-prefix"))
-    no_pci: Switch
-    vdevs: list[VirtualDevice] | None = field(
-        default=None, metadata=Params.multiple() | Params.long("vdev")
-    )
-    ports: list[Port] | None = field(
-        default=None,
-        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
-    )
-    other_eal_param: Params | None = None
-    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
-
-
 class SutNode(Node):
     """The system under test node.
 
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v4 4/8] dts: remove module-wide imports
  2024-06-17 14:42 ` [PATCH v4 0/8] dts: add testpmd params and statefulness Luca Vizzarro
                     ` (2 preceding siblings ...)
  2024-06-17 14:42   ` [PATCH v4 3/8] dts: refactor EalParams Luca Vizzarro
@ 2024-06-17 14:42   ` Luca Vizzarro
  2024-06-17 14:42   ` [PATCH v4 5/8] dts: add testpmd shell params Luca Vizzarro
                     ` (3 subsequent siblings)
  7 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-17 14:42 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Remove the imports in the testbed_model and remote_session modules init
file, to avoid the initialisation of unneeded modules, thus removing or
limiting the risk of circular dependencies.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 dts/framework/remote_session/__init__.py               | 7 +------
 dts/framework/runner.py                                | 4 +++-
 dts/framework/test_suite.py                            | 5 ++++-
 dts/framework/testbed_model/__init__.py                | 9 ---------
 dts/framework/testbed_model/os_session.py              | 4 ++--
 dts/framework/testbed_model/sut_node.py                | 2 +-
 dts/framework/testbed_model/traffic_generator/scapy.py | 2 +-
 dts/tests/TestSuite_hello_world.py                     | 2 +-
 dts/tests/TestSuite_smoke_tests.py                     | 2 +-
 9 files changed, 14 insertions(+), 23 deletions(-)

diff --git a/dts/framework/remote_session/__init__.py b/dts/framework/remote_session/__init__.py
index 1910c81c3c..0668e9c884 100644
--- a/dts/framework/remote_session/__init__.py
+++ b/dts/framework/remote_session/__init__.py
@@ -12,17 +12,12 @@
 allowing it to send and receive data within that particular shell.
 """
 
-# pylama:ignore=W0611
-
 from framework.config import NodeConfiguration
 from framework.logger import DTSLogger
 
 from .interactive_remote_session import InteractiveRemoteSession
-from .interactive_shell import InteractiveShell
-from .python_shell import PythonShell
-from .remote_session import CommandResult, RemoteSession
+from .remote_session import RemoteSession
 from .ssh_session import SSHSession
-from .testpmd_shell import TestPmdShell
 
 
 def create_remote_session(
diff --git a/dts/framework/runner.py b/dts/framework/runner.py
index dfdee14802..687bc04f79 100644
--- a/dts/framework/runner.py
+++ b/dts/framework/runner.py
@@ -26,6 +26,9 @@
 from types import FunctionType
 from typing import Iterable, Sequence
 
+from framework.testbed_model.sut_node import SutNode
+from framework.testbed_model.tg_node import TGNode
+
 from .config import (
     BuildTargetConfiguration,
     Configuration,
@@ -51,7 +54,6 @@
     TestSuiteWithCases,
 )
 from .test_suite import TestSuite
-from .testbed_model import SutNode, TGNode
 
 
 class DTSRunner:
diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py
index 8768f756a6..9d3debb00f 100644
--- a/dts/framework/test_suite.py
+++ b/dts/framework/test_suite.py
@@ -20,9 +20,12 @@
 from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
 from scapy.packet import Packet, Padding  # type: ignore[import-untyped]
 
+from framework.testbed_model.port import Port, PortLink
+from framework.testbed_model.sut_node import SutNode
+from framework.testbed_model.tg_node import TGNode
+
 from .exception import TestCaseVerifyError
 from .logger import DTSLogger, get_dts_logger
-from .testbed_model import Port, PortLink, SutNode, TGNode
 from .testbed_model.traffic_generator import PacketFilteringConfig
 from .utils import get_packet_summaries
 
diff --git a/dts/framework/testbed_model/__init__.py b/dts/framework/testbed_model/__init__.py
index 6086512ca2..e3edd4d811 100644
--- a/dts/framework/testbed_model/__init__.py
+++ b/dts/framework/testbed_model/__init__.py
@@ -17,12 +17,3 @@
 DTS needs to be able to connect to nodes and understand some of the hardware present on these nodes
 to properly build and test DPDK.
 """
-
-# pylama:ignore=W0611
-
-from .cpu import LogicalCoreCount, LogicalCoreCountFilter, LogicalCoreList
-from .node import Node
-from .port import Port, PortLink
-from .sut_node import SutNode
-from .tg_node import TGNode
-from .virtual_device import VirtualDevice
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
index 1a77aee532..e5f5fcbe0e 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -32,13 +32,13 @@
 from framework.logger import DTSLogger
 from framework.params import Params
 from framework.remote_session import (
-    CommandResult,
     InteractiveRemoteSession,
-    InteractiveShell,
     RemoteSession,
     create_interactive_session,
     create_remote_session,
 )
+from framework.remote_session.interactive_shell import InteractiveShell
+from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
 
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index e1163106a3..83ad06ae2d 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -26,7 +26,7 @@
 )
 from framework.params import Params, Switch
 from framework.params.eal import EalParams
-from framework.remote_session import CommandResult
+from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
 
diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py
index ed5467d825..7bc1c2cc08 100644
--- a/dts/framework/testbed_model/traffic_generator/scapy.py
+++ b/dts/framework/testbed_model/traffic_generator/scapy.py
@@ -25,7 +25,7 @@
 from scapy.packet import Packet  # type: ignore[import-untyped]
 
 from framework.config import OS, ScapyTrafficGeneratorConfig
-from framework.remote_session import PythonShell
+from framework.remote_session.python_shell import PythonShell
 from framework.settings import SETTINGS
 from framework.testbed_model.node import Node
 from framework.testbed_model.port import Port
diff --git a/dts/tests/TestSuite_hello_world.py b/dts/tests/TestSuite_hello_world.py
index fd7ff1534d..0d6995f260 100644
--- a/dts/tests/TestSuite_hello_world.py
+++ b/dts/tests/TestSuite_hello_world.py
@@ -8,7 +8,7 @@
 """
 
 from framework.test_suite import TestSuite
-from framework.testbed_model import (
+from framework.testbed_model.cpu import (
     LogicalCoreCount,
     LogicalCoreCountFilter,
     LogicalCoreList,
diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py
index a553e89662..ca678f662d 100644
--- a/dts/tests/TestSuite_smoke_tests.py
+++ b/dts/tests/TestSuite_smoke_tests.py
@@ -15,7 +15,7 @@
 import re
 
 from framework.config import PortConfig
-from framework.remote_session import TestPmdShell
+from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.settings import SETTINGS
 from framework.test_suite import TestSuite
 from framework.utils import REGEX_FOR_PCI_ADDRESS
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v4 5/8] dts: add testpmd shell params
  2024-06-17 14:42 ` [PATCH v4 0/8] dts: add testpmd params and statefulness Luca Vizzarro
                     ` (3 preceding siblings ...)
  2024-06-17 14:42   ` [PATCH v4 4/8] dts: remove module-wide imports Luca Vizzarro
@ 2024-06-17 14:42   ` Luca Vizzarro
  2024-06-17 14:42   ` [PATCH v4 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
                     ` (2 subsequent siblings)
  7 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-17 14:42 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Implement all the testpmd shell parameters into a data structure.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 dts/framework/params/testpmd.py               | 607 ++++++++++++++++++
 dts/framework/remote_session/testpmd_shell.py |  39 +-
 dts/tests/TestSuite_pmd_buffer_scatter.py     |   5 +-
 3 files changed, 613 insertions(+), 38 deletions(-)
 create mode 100644 dts/framework/params/testpmd.py

diff --git a/dts/framework/params/testpmd.py b/dts/framework/params/testpmd.py
new file mode 100644
index 0000000000..1913bd0fa2
--- /dev/null
+++ b/dts/framework/params/testpmd.py
@@ -0,0 +1,607 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Module containing all the TestPmd-related parameter classes."""
+
+from dataclasses import dataclass, field
+from enum import EnumMeta, Flag, auto, unique
+from pathlib import PurePath
+from typing import Literal, NamedTuple
+
+from framework.params import (
+    Params,
+    Switch,
+    YesNoSwitch,
+    bracketed,
+    comma_separated,
+    hex_from_flag_value,
+    modify_str,
+    str_from_flag_value,
+)
+from framework.params.eal import EalParams
+from framework.utils import StrEnum
+
+
+class PortTopology(StrEnum):
+    """Enum representing the port topology."""
+
+    #: In paired mode, the forwarding is between pairs of ports, e.g.: (0,1), (2,3), (4,5).
+    paired = auto()
+
+    #: In chained mode, the forwarding is to the next available port in the port mask, e.g.:
+    #: (0,1), (1,2), (2,0).
+    #:
+    #: The ordering of the ports can be changed using the portlist testpmd runtime function.
+    chained = auto()
+
+    #: In loop mode, ingress traffic is simply transmitted back on the same interface.
+    loop = auto()
+
+
+@modify_str(comma_separated, bracketed)
+class PortNUMAConfig(NamedTuple):
+    """DPDK port to NUMA socket association tuple."""
+
+    #:
+    port: int
+    #:
+    socket: int
+
+
+@modify_str(str_from_flag_value)
+@unique
+class FlowDirection(Flag):
+    """Flag indicating the direction of the flow.
+
+    A bi-directional flow can be specified with the pipe:
+
+    >>> TestPmdFlowDirection.RX | TestPmdFlowDirection.TX
+    <TestPmdFlowDirection.TX|RX: 3>
+    """
+
+    #:
+    RX = 1 << 0
+    #:
+    TX = 1 << 1
+
+
+@modify_str(comma_separated, bracketed)
+class RingNUMAConfig(NamedTuple):
+    """Tuple associating DPDK port, direction of the flow and NUMA socket."""
+
+    #:
+    port: int
+    #:
+    direction: FlowDirection
+    #:
+    socket: int
+
+
+@modify_str(comma_separated)
+class EthPeer(NamedTuple):
+    """Tuple associating a MAC address to the specified DPDK port."""
+
+    #:
+    port_no: int
+    #:
+    mac_address: str
+
+
+@modify_str(comma_separated)
+class TxIPAddrPair(NamedTuple):
+    """Tuple specifying the source and destination IPs for the packets."""
+
+    #:
+    source_ip: str
+    #:
+    dest_ip: str
+
+
+@modify_str(comma_separated)
+class TxUDPPortPair(NamedTuple):
+    """Tuple specifying the UDP source and destination ports for the packets.
+
+    If leaving ``dest_port`` unspecified, ``source_port`` will be used for
+    the destination port as well.
+    """
+
+    #:
+    source_port: int
+    #:
+    dest_port: int | None = None
+
+
+@dataclass
+class DisableRSS(Params):
+    """Disables RSS (Receive Side Scaling)."""
+
+    _disable_rss: Literal[True] = field(
+        default=True, init=False, metadata=Params.long("disable-rss")
+    )
+
+
+@dataclass
+class SetRSSIPOnly(Params):
+    """Sets RSS (Receive Side Scaling) functions for IPv4/IPv6 only."""
+
+    _rss_ip: Literal[True] = field(default=True, init=False, metadata=Params.long("rss-ip"))
+
+
+@dataclass
+class SetRSSUDP(Params):
+    """Sets RSS (Receive Side Scaling) functions for IPv4/IPv6 and UDP."""
+
+    _rss_udp: Literal[True] = field(default=True, init=False, metadata=Params.long("rss-udp"))
+
+
+class RSSSetting(EnumMeta):
+    """Enum representing a RSS setting. Each property is a class that needs to be initialised."""
+
+    #:
+    Disabled = DisableRSS
+    #:
+    SetIPOnly = SetRSSIPOnly
+    #:
+    SetUDP = SetRSSUDP
+
+
+class SimpleForwardingModes(StrEnum):
+    r"""The supported packet forwarding modes for :class:`~TestPmdShell`\s."""
+
+    #:
+    io = auto()
+    #:
+    mac = auto()
+    #:
+    macswap = auto()
+    #:
+    rxonly = auto()
+    #:
+    csum = auto()
+    #:
+    icmpecho = auto()
+    #:
+    ieee1588 = auto()
+    #:
+    fivetswap = "5tswap"
+    #:
+    shared_rxq = "shared-rxq"
+    #:
+    recycle_mbufs = auto()
+
+
+@dataclass(kw_only=True)
+class TXOnlyForwardingMode(Params):
+    """Sets a TX-Only forwarding mode.
+
+    Attributes:
+        multi_flow: Generates multiple flows if set to True.
+        segments_length: Sets TX segment sizes or total packet length.
+    """
+
+    _forward_mode: Literal["txonly"] = field(
+        default="txonly", init=False, metadata=Params.long("forward-mode")
+    )
+    multi_flow: Switch = field(default=None, metadata=Params.long("txonly-multi-flow"))
+    segments_length: list[int] | None = field(
+        default=None, metadata=Params.long("txpkts") | Params.convert_value(comma_separated)
+    )
+
+
+@dataclass(kw_only=True)
+class FlowGenForwardingMode(Params):
+    """Sets a flowgen forwarding mode.
+
+    Attributes:
+        clones: Set the number of each packet clones to be sent. Sending clones reduces host CPU
+                load on creating packets and may help in testing extreme speeds or maxing out
+                Tx packet performance. N should be not zero, but less than ‘burst’ parameter.
+        flows: Set the number of flows to be generated, where 1 <= N <= INT32_MAX.
+        segments_length: Set TX segment sizes or total packet length.
+    """
+
+    _forward_mode: Literal["flowgen"] = field(
+        default="flowgen", init=False, metadata=Params.long("forward-mode")
+    )
+    clones: int | None = field(default=None, metadata=Params.long("flowgen-clones"))
+    flows: int | None = field(default=None, metadata=Params.long("flowgen-flows"))
+    segments_length: list[int] | None = field(
+        default=None, metadata=Params.long("txpkts") | Params.convert_value(comma_separated)
+    )
+
+
+@dataclass(kw_only=True)
+class NoisyForwardingMode(Params):
+    """Sets a noisy forwarding mode.
+
+    Attributes:
+        forward_mode: Set the noisy VNF forwarding mode.
+        tx_sw_buffer_size: Set the maximum number of elements of the FIFO queue to be created for
+                           buffering packets.
+        tx_sw_buffer_flushtime: Set the time before packets in the FIFO queue are flushed.
+        lkup_memory: Set the size of the noisy neighbor simulation memory buffer in MB to N.
+        lkup_num_reads: Set the size of the noisy neighbor simulation memory buffer in MB to N.
+        lkup_num_writes: Set the number of writes to be done in noisy neighbor simulation
+                         memory buffer to N.
+        lkup_num_reads_writes: Set the number of r/w accesses to be done in noisy neighbor
+                               simulation memory buffer to N.
+    """
+
+    _forward_mode: Literal["noisy"] = field(
+        default="noisy", init=False, metadata=Params.long("forward-mode")
+    )
+    forward_mode: (
+        Literal[
+            SimpleForwardingModes.io,
+            SimpleForwardingModes.mac,
+            SimpleForwardingModes.macswap,
+            SimpleForwardingModes.fivetswap,
+        ]
+        | None
+    ) = field(default=SimpleForwardingModes.io, metadata=Params.long("noisy-forward-mode"))
+    tx_sw_buffer_size: int | None = field(
+        default=None, metadata=Params.long("noisy-tx-sw-buffer-size")
+    )
+    tx_sw_buffer_flushtime: int | None = field(
+        default=None, metadata=Params.long("noisy-tx-sw-buffer-flushtime")
+    )
+    lkup_memory: int | None = field(default=None, metadata=Params.long("noisy-lkup-memory"))
+    lkup_num_reads: int | None = field(default=None, metadata=Params.long("noisy-lkup-num-reads"))
+    lkup_num_writes: int | None = field(default=None, metadata=Params.long("noisy-lkup-num-writes"))
+    lkup_num_reads_writes: int | None = field(
+        default=None, metadata=Params.long("noisy-lkup-num-reads-writes")
+    )
+
+
+@modify_str(hex_from_flag_value)
+@unique
+class HairpinMode(Flag):
+    """Flag representing the hairpin mode."""
+
+    #: Two hairpin ports loop.
+    TWO_PORTS_LOOP = 1 << 0
+    #: Two hairpin ports paired.
+    TWO_PORTS_PAIRED = 1 << 1
+    #: Explicit Tx flow rule.
+    EXPLICIT_TX_FLOW = 1 << 4
+    #: Force memory settings of hairpin RX queue.
+    FORCE_RX_QUEUE_MEM_SETTINGS = 1 << 8
+    #: Force memory settings of hairpin TX queue.
+    FORCE_TX_QUEUE_MEM_SETTINGS = 1 << 9
+    #: Hairpin RX queues will use locked device memory.
+    RX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 12
+    #: Hairpin RX queues will use RTE memory.
+    RX_QUEUE_USE_RTE_MEMORY = 1 << 13
+    #: Hairpin TX queues will use locked device memory.
+    TX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 16
+    #: Hairpin TX queues will use RTE memory.
+    TX_QUEUE_USE_RTE_MEMORY = 1 << 18
+
+
+@dataclass(kw_only=True)
+class RXRingParams(Params):
+    """Sets the RX ring parameters.
+
+    Attributes:
+        descriptors: Set the number of descriptors in the RX rings to N, where N > 0.
+        prefetch_threshold: Set the prefetch threshold register of RX rings to N, where N >= 0.
+        host_threshold: Set the host threshold register of RX rings to N, where N >= 0.
+        write_back_threshold: Set the write-back threshold register of RX rings to N, where N >= 0.
+        free_threshold: Set the free threshold of RX descriptors to N,
+                        where 0 <= N < value of ``-–rxd``.
+    """
+
+    descriptors: int | None = field(default=None, metadata=Params.long("rxd"))
+    prefetch_threshold: int | None = field(default=None, metadata=Params.long("rxpt"))
+    host_threshold: int | None = field(default=None, metadata=Params.long("rxht"))
+    write_back_threshold: int | None = field(default=None, metadata=Params.long("rxwt"))
+    free_threshold: int | None = field(default=None, metadata=Params.long("rxfreet"))
+
+
+@modify_str(hex_from_flag_value)
+@unique
+class RXMultiQueueMode(Flag):
+    """Flag representing the RX multi-queue mode."""
+
+    #:
+    RSS = 1 << 0
+    #:
+    DCB = 1 << 1
+    #:
+    VMDQ = 1 << 2
+
+
+@dataclass(kw_only=True)
+class TXRingParams(Params):
+    """Sets the TX ring parameters.
+
+    Attributes:
+        descriptors: Set the number of descriptors in the TX rings to N, where N > 0.
+        rs_bit_threshold: Set the transmit RS bit threshold of TX rings to N,
+                          where 0 <= N <= value of ``--txd``.
+        prefetch_threshold: Set the prefetch threshold register of TX rings to N, where N >= 0.
+        host_threshold: Set the host threshold register of TX rings to N, where N >= 0.
+        write_back_threshold: Set the write-back threshold register of TX rings to N, where N >= 0.
+        free_threshold: Set the transmit free threshold of TX rings to N,
+                        where 0 <= N <= value of ``--txd``.
+    """
+
+    descriptors: int | None = field(default=None, metadata=Params.long("txd"))
+    rs_bit_threshold: int | None = field(default=None, metadata=Params.long("txrst"))
+    prefetch_threshold: int | None = field(default=None, metadata=Params.long("txpt"))
+    host_threshold: int | None = field(default=None, metadata=Params.long("txht"))
+    write_back_threshold: int | None = field(default=None, metadata=Params.long("txwt"))
+    free_threshold: int | None = field(default=None, metadata=Params.long("txfreet"))
+
+
+class Event(StrEnum):
+    """Enum representing a testpmd event."""
+
+    #:
+    unknown = auto()
+    #:
+    queue_state = auto()
+    #:
+    vf_mbox = auto()
+    #:
+    macsec = auto()
+    #:
+    intr_lsc = auto()
+    #:
+    intr_rmv = auto()
+    #:
+    intr_reset = auto()
+    #:
+    dev_probed = auto()
+    #:
+    dev_released = auto()
+    #:
+    flow_aged = auto()
+    #:
+    err_recovering = auto()
+    #:
+    recovery_success = auto()
+    #:
+    recovery_failed = auto()
+    #:
+    all = auto()
+
+
+class SimpleMempoolAllocationMode(StrEnum):
+    """Enum representing simple mempool allocation modes."""
+
+    #: Create and populate mempool using native DPDK memory.
+    native = auto()
+    #: Create and populate mempool using externally and anonymously allocated area.
+    xmem = auto()
+    #: Create and populate mempool using externally and anonymously allocated hugepage area.
+    xmemhuge = auto()
+
+
+@dataclass(kw_only=True)
+class AnonMempoolAllocationMode(Params):
+    """Create mempool using native DPDK memory, but populate using anonymous memory.
+
+    Attributes:
+        no_iova_contig: Enables to create mempool which is not IOVA contiguous.
+    """
+
+    _mp_alloc: Literal["anon"] = field(default="anon", init=False, metadata=Params.long("mp-alloc"))
+    no_iova_contig: Switch = None
+
+
+@dataclass(slots=True, kw_only=True)
+class TestPmdParams(EalParams):
+    """The testpmd shell parameters.
+
+    Attributes:
+        interactive_mode: Run testpmd in interactive mode.
+        auto_start: Start forwarding on initialization.
+        tx_first: Start forwarding, after sending a burst of packets first.
+        stats_period: Display statistics every ``PERIOD`` seconds, if interactive mode is disabled.
+                      The default value is 0, which means that the statistics will not be displayed.
+
+                      .. note:: This flag should be used only in non-interactive mode.
+        display_xstats: Display comma-separated list of extended statistics every ``PERIOD`` seconds
+                        as specified in ``--stats-period`` or when used with interactive commands
+                        that show Rx/Tx statistics (i.e. ‘show port stats’).
+        nb_cores: Set the number of forwarding cores, where 1 <= N <= “number of cores” or
+                  ``RTE_MAX_LCORE`` from the configuration file.
+        coremask: Set the bitmask of the cores running the packet forwarding test. The main
+                  lcore is reserved for command line parsing only and cannot be masked on for packet
+                  forwarding.
+        nb_ports: Set the number of forwarding ports, where 1 <= N <= “number of ports” on the board
+                  or ``RTE_MAX_ETHPORTS`` from the configuration file. The default value is the
+                  number of ports on the board.
+        port_topology: Set port topology, where mode is paired (the default), chained or loop.
+        portmask: Set the bitmask of the ports used by the packet forwarding test.
+        portlist: Set the forwarding ports based on the user input used by the packet forwarding
+                  test. ‘-‘ denotes a range of ports to set including the two specified port IDs ‘,’
+                  separates multiple port values. Possible examples like –portlist=0,1 or
+                  –portlist=0-2 or –portlist=0,1-2 etc.
+        numa: Enable/disable NUMA-aware allocation of RX/TX rings and of RX memory buffers (mbufs).
+        socket_num: Set the socket from which all memory is allocated in NUMA mode, where
+                    0 <= N < number of sockets on the board.
+        port_numa_config: Specify the socket on which the memory pool to be used by the port will be
+                          allocated.
+        ring_numa_config: Specify the socket on which the TX/RX rings for the port will be
+                          allocated. Where flag is 1 for RX, 2 for TX, and 3 for RX and TX.
+        total_num_mbufs: Set the number of mbufs to be allocated in the mbuf pools, where N > 1024.
+        mbuf_size: Set the data size of the mbufs used to N bytes, where N < 65536.
+                   If multiple mbuf-size values are specified the extra memory pools will be created
+                   for allocating mbufs to receive packets with buffer splitting features.
+        mbcache: Set the cache of mbuf memory pools to N, where 0 <= N <= 512.
+        max_pkt_len: Set the maximum packet size to N bytes, where N >= 64.
+        eth_peers_configfile: Use a configuration file containing the Ethernet addresses of
+                              the peer ports.
+        eth_peer: Set the MAC address XX:XX:XX:XX:XX:XX of the peer port N,
+                  where 0 <= N < RTE_MAX_ETHPORTS.
+        tx_ip: Set the source and destination IP address used when doing transmit only test.
+               The defaults address values are source 198.18.0.1 and destination 198.18.0.2.
+               These are special purpose addresses reserved for benchmarking (RFC 5735).
+        tx_udp: Set the source and destination UDP port number for transmit test only test.
+                The default port is the port 9 which is defined for the discard protocol (RFC 863).
+        enable_lro: Enable large receive offload.
+        max_lro_pkt_size: Set the maximum LRO aggregated packet size to N bytes, where N >= 64.
+        disable_crc_strip: Disable hardware CRC stripping.
+        enable_scatter: Enable scatter (multi-segment) RX.
+        enable_hw_vlan: Enable hardware VLAN.
+        enable_hw_vlan_filter: Enable hardware VLAN filter.
+        enable_hw_vlan_strip: Enable hardware VLAN strip.
+        enable_hw_vlan_extend: Enable hardware VLAN extend.
+        enable_hw_qinq_strip: Enable hardware QINQ strip.
+        pkt_drop_enabled: Enable per-queue packet drop for packets with no descriptors.
+        rss: Receive Side Scaling setting.
+        forward_mode: Set the forwarding mode.
+        hairpin_mode: Set the hairpin port configuration.
+        hairpin_queues: Set the number of hairpin queues per port to N, where 1 <= N <= 65535.
+        burst: Set the number of packets per burst to N, where 1 <= N <= 512.
+        enable_rx_cksum: Enable hardware RX checksum offload.
+        rx_queues: Set the number of RX queues per port to N, where 1 <= N <= 65535.
+        rx_ring: Set the RX rings parameters.
+        no_flush_rx: Don’t flush the RX streams before starting forwarding. Used mainly with
+                     the PCAP PMD.
+        rx_segments_offsets: Set the offsets of packet segments on receiving
+                             if split feature is engaged.
+        rx_segments_length: Set the length of segments to scatter packets on receiving
+                            if split feature is engaged.
+        multi_rx_mempool: Enable multiple mbuf pools per Rx queue.
+        rx_shared_queue: Create queues in shared Rx queue mode if device supports. Shared Rx queues
+                         are grouped per X ports. X defaults to UINT32_MAX, implies all ports join
+                         share group 1. Forwarding engine “shared-rxq” should be used for shared Rx
+                         queues. This engine does Rx only and update stream statistics accordingly.
+        rx_offloads: Set the bitmask of RX queue offloads.
+        rx_mq_mode: Set the RX multi queue mode which can be enabled.
+        tx_queues: Set the number of TX queues per port to N, where 1 <= N <= 65535.
+        tx_ring: Set the TX rings params.
+        tx_offloads: Set the hexadecimal bitmask of TX queue offloads.
+        eth_link_speed: Set a forced link speed to the ethernet port. E.g. 1000 for 1Gbps.
+        disable_link_check: Disable check on link status when starting/stopping ports.
+        disable_device_start: Do not automatically start all ports. This allows testing
+                              configuration of rx and tx queues before device is started
+                              for the first time.
+        no_lsc_interrupt: Disable LSC interrupts for all ports, even those supporting it.
+        no_rmv_interrupt: Disable RMV interrupts for all ports, even those supporting it.
+        bitrate_stats: Set the logical core N to perform bitrate calculation.
+        latencystats: Set the logical core N to perform latency and jitter calculations.
+        print_events: Enable printing the occurrence of the designated events.
+                      Using :attr:`TestPmdEvent.ALL` will enable all of them.
+        mask_events: Disable printing the occurrence of the designated events.
+                     Using :attr:`TestPmdEvent.ALL` will disable all of them.
+        flow_isolate_all: Providing this parameter requests flow API isolated mode on all ports at
+                          initialization time. It ensures all traffic is received through the
+                          configured flow rules only (see flow command). Ports that do not support
+                          this mode are automatically discarded.
+        disable_flow_flush: Disable port flow flush when stopping port.
+                            This allows testing keep flow rules or shared flow objects across
+                            restart.
+        hot_plug: Enable device event monitor mechanism for hotplug.
+        vxlan_gpe_port: Set the UDP port number of tunnel VXLAN-GPE to N.
+        geneve_parsed_port: Set the UDP port number that is used for parsing the GENEVE protocol
+                            to N. HW may be configured with another tunnel Geneve port.
+        lock_all_memory: Enable/disable locking all memory. Disabled by default.
+        mempool_allocation_mode: Set mempool allocation mode.
+        record_core_cycles: Enable measurement of CPU cycles per packet.
+        record_burst_status: Enable display of RX and TX burst stats.
+    """
+
+    interactive_mode: Switch = field(default=True, metadata=Params.short("i"))
+    auto_start: Switch = field(default=None, metadata=Params.short("a"))
+    tx_first: Switch = None
+    stats_period: int | None = None
+    display_xstats: list[str] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    nb_cores: int | None = None
+    coremask: int | None = field(default=None, metadata=Params.convert_value(hex))
+    nb_ports: int | None = None
+    port_topology: PortTopology | None = PortTopology.paired
+    portmask: int | None = field(default=None, metadata=Params.convert_value(hex))
+    portlist: str | None = None  # TODO: can be ranges 0,1-3
+
+    numa: YesNoSwitch = None
+    socket_num: int | None = None
+    port_numa_config: list[PortNUMAConfig] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    ring_numa_config: list[RingNUMAConfig] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    total_num_mbufs: int | None = None
+    mbuf_size: list[int] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    mbcache: int | None = None
+    max_pkt_len: int | None = None
+    eth_peers_configfile: PurePath | None = None
+    eth_peer: list[EthPeer] | None = field(default=None, metadata=Params.multiple())
+    tx_ip: TxIPAddrPair | None = None
+    tx_udp: TxUDPPortPair | None = None
+    enable_lro: Switch = None
+    max_lro_pkt_size: int | None = None
+    disable_crc_strip: Switch = None
+    enable_scatter: Switch = None
+    enable_hw_vlan: Switch = None
+    enable_hw_vlan_filter: Switch = None
+    enable_hw_vlan_strip: Switch = None
+    enable_hw_vlan_extend: Switch = None
+    enable_hw_qinq_strip: Switch = None
+    pkt_drop_enabled: Switch = field(default=None, metadata=Params.long("enable-drop-en"))
+    rss: RSSSetting | None = None
+    forward_mode: (
+        SimpleForwardingModes
+        | FlowGenForwardingMode
+        | TXOnlyForwardingMode
+        | NoisyForwardingMode
+        | None
+    ) = None
+    hairpin_mode: HairpinMode | None = None
+    hairpin_queues: int | None = field(default=None, metadata=Params.long("hairpinq"))
+    burst: int | None = None
+    enable_rx_cksum: Switch = None
+
+    rx_queues: int | None = field(default=None, metadata=Params.long("rxq"))
+    rx_ring: RXRingParams | None = None
+    no_flush_rx: Switch = None
+    rx_segments_offsets: list[int] | None = field(
+        default=None, metadata=Params.long("rxoffs") | Params.convert_value(comma_separated)
+    )
+    rx_segments_length: list[int] | None = field(
+        default=None, metadata=Params.long("rxpkts") | Params.convert_value(comma_separated)
+    )
+    multi_rx_mempool: Switch = None
+    rx_shared_queue: Switch | int = field(default=None, metadata=Params.long("rxq-share"))
+    rx_offloads: int | None = field(default=None, metadata=Params.convert_value(hex))
+    rx_mq_mode: RXMultiQueueMode | None = None
+
+    tx_queues: int | None = field(default=None, metadata=Params.long("txq"))
+    tx_ring: TXRingParams | None = None
+    tx_offloads: int | None = field(default=None, metadata=Params.convert_value(hex))
+
+    eth_link_speed: int | None = None
+    disable_link_check: Switch = None
+    disable_device_start: Switch = None
+    no_lsc_interrupt: Switch = None
+    no_rmv_interrupt: Switch = None
+    bitrate_stats: int | None = None
+    latencystats: int | None = None
+    print_events: list[Event] | None = field(
+        default=None, metadata=Params.multiple() | Params.long("print-event")
+    )
+    mask_events: list[Event] | None = field(
+        default_factory=lambda: [Event.intr_lsc],
+        metadata=Params.multiple() | Params.long("mask-event"),
+    )
+
+    flow_isolate_all: Switch = None
+    disable_flow_flush: Switch = None
+
+    hot_plug: Switch = None
+    vxlan_gpe_port: int | None = None
+    geneve_parsed_port: int | None = None
+    lock_all_memory: YesNoSwitch = field(default=None, metadata=Params.long("mlockall"))
+    mempool_allocation_mode: SimpleMempoolAllocationMode | AnonMempoolAllocationMode | None = field(
+        default=None, metadata=Params.long("mp-alloc")
+    )
+    record_core_cycles: Switch = None
+    record_burst_status: Switch = None
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index 2b9ef9418d..82701a9839 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -26,7 +26,7 @@
 from typing_extensions import Self
 
 from framework.exception import InteractiveCommandExecutionError
-from framework.params.eal import EalParams
+from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
 from framework.parser import ParserFn, TextParser
 from framework.settings import SETTINGS
 from framework.utils import StrEnum
@@ -56,37 +56,6 @@ def __str__(self) -> str:
         return self.pci_address
 
 
-class TestPmdForwardingModes(StrEnum):
-    r"""The supported packet forwarding modes for :class:`~TestPmdShell`\s."""
-
-    #:
-    io = auto()
-    #:
-    mac = auto()
-    #:
-    macswap = auto()
-    #:
-    flowgen = auto()
-    #:
-    rxonly = auto()
-    #:
-    txonly = auto()
-    #:
-    csum = auto()
-    #:
-    icmpecho = auto()
-    #:
-    ieee1588 = auto()
-    #:
-    noisy = auto()
-    #:
-    fivetswap = "5tswap"
-    #:
-    shared_rxq = "shared-rxq"
-    #:
-    recycle_mbufs = auto()
-
-
 class VLANOffloadFlag(Flag):
     """Flag representing the VLAN offload settings of a NIC port."""
 
@@ -646,9 +615,7 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
         Also find the number of pci addresses which were allowed on the command line when the app
         was started.
         """
-        self._app_params += " -i --mask-event intr_lsc"
-
-        assert isinstance(self._app_params, EalParams)
+        assert isinstance(self._app_params, TestPmdParams)
 
         self.number_of_ports = (
             len(self._app_params.ports) if self._app_params.ports is not None else 0
@@ -740,7 +707,7 @@ def wait_link_status_up(self, port_id: int, timeout=SETTINGS.timeout) -> bool:
             self._logger.error(f"The link for port {port_id} did not come up in the given timeout.")
         return "Link status: up" in port_info
 
-    def set_forward_mode(self, mode: TestPmdForwardingModes, verify: bool = True):
+    def set_forward_mode(self, mode: SimpleForwardingModes, verify: bool = True):
         """Set packet forwarding mode.
 
         Args:
diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index c6e93839cb..578b5a4318 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -23,7 +23,8 @@
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
 from framework.params import Params
-from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
+from framework.params.testpmd import SimpleForwardingModes
+from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.test_suite import TestSuite
 
 
@@ -113,7 +114,7 @@ def pmd_scatter(self, mbsize: int) -> None:
             ),
             privileged=True,
         )
-        testpmd.set_forward_mode(TestPmdForwardingModes.mac)
+        testpmd.set_forward_mode(SimpleForwardingModes.mac)
         testpmd.start()
 
         for offset in [-1, 0, 1, 4, 5]:
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v4 6/8] dts: use testpmd params for scatter test suite
  2024-06-17 14:42 ` [PATCH v4 0/8] dts: add testpmd params and statefulness Luca Vizzarro
                     ` (4 preceding siblings ...)
  2024-06-17 14:42   ` [PATCH v4 5/8] dts: add testpmd shell params Luca Vizzarro
@ 2024-06-17 14:42   ` Luca Vizzarro
  2024-06-17 14:42   ` [PATCH v4 7/8] dts: rework interactive shells Luca Vizzarro
  2024-06-17 14:42   ` [PATCH v4 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
  7 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-17 14:42 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Update the buffer scatter test suite to use TestPmdParameters
instead of the StrParams implementation.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 dts/tests/TestSuite_pmd_buffer_scatter.py | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index 578b5a4318..6d206c1a40 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -16,14 +16,14 @@
 """
 
 import struct
+from dataclasses import asdict
 
 from scapy.layers.inet import IP  # type: ignore[import-untyped]
 from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
 from scapy.packet import Raw  # type: ignore[import-untyped]
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
-from framework.params import Params
-from framework.params.testpmd import SimpleForwardingModes
+from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
 from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.test_suite import TestSuite
 
@@ -105,16 +105,16 @@ def pmd_scatter(self, mbsize: int) -> None:
         """
         testpmd = self.sut_node.create_interactive_shell(
             TestPmdShell,
-            app_params=Params.from_str(
-                "--mbcache=200 "
-                f"--mbuf-size={mbsize} "
-                "--max-pkt-len=9000 "
-                "--port-topology=paired "
-                "--tx-offloads=0x00008000"
+            app_params=TestPmdParams(
+                forward_mode=SimpleForwardingModes.mac,
+                mbcache=200,
+                mbuf_size=[mbsize],
+                max_pkt_len=9000,
+                tx_offloads=0x00008000,
+                **asdict(self.sut_node.create_eal_parameters()),
             ),
             privileged=True,
         )
-        testpmd.set_forward_mode(SimpleForwardingModes.mac)
         testpmd.start()
 
         for offset in [-1, 0, 1, 4, 5]:
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v4 7/8] dts: rework interactive shells
  2024-06-17 14:42 ` [PATCH v4 0/8] dts: add testpmd params and statefulness Luca Vizzarro
                     ` (5 preceding siblings ...)
  2024-06-17 14:42   ` [PATCH v4 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
@ 2024-06-17 14:42   ` Luca Vizzarro
  2024-06-17 14:42   ` [PATCH v4 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
  7 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-17 14:42 UTC (permalink / raw)
  To: dev; +Cc: Jeremy Spewock, Juraj Linkeš, Luca Vizzarro, Paul Szczepanek

The way nodes and interactive shells interact makes it difficult to
develop for static type checking and hinting. The current system relies
on a top-down approach, attempting to give a generic interface to the
test developer, hiding the interaction of concrete shell classes as much
as possible. When working with strong typing this approach is not ideal,
as Python's implementation of generics is still rudimentary.

This rework reverses the tests interaction to a bottom-up approach,
allowing the test developer to call concrete shell classes directly,
and let them ingest nodes independently. While also re-enforcing type
checking and making the code easier to read.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 dts/framework/params/eal.py                   |   6 +-
 dts/framework/remote_session/dpdk_shell.py    | 106 +++++++++++++++
 .../remote_session/interactive_shell.py       |  79 ++++++-----
 dts/framework/remote_session/python_shell.py  |   4 +-
 dts/framework/remote_session/testpmd_shell.py |  64 +++++----
 dts/framework/testbed_model/node.py           |  36 +----
 dts/framework/testbed_model/os_session.py     |  36 +----
 dts/framework/testbed_model/sut_node.py       | 124 +-----------------
 .../testbed_model/traffic_generator/scapy.py  |   4 +-
 dts/tests/TestSuite_hello_world.py            |   7 +-
 dts/tests/TestSuite_pmd_buffer_scatter.py     |  21 ++-
 dts/tests/TestSuite_smoke_tests.py            |   2 +-
 12 files changed, 210 insertions(+), 279 deletions(-)
 create mode 100644 dts/framework/remote_session/dpdk_shell.py

diff --git a/dts/framework/params/eal.py b/dts/framework/params/eal.py
index bbdbc8f334..8d7766fefc 100644
--- a/dts/framework/params/eal.py
+++ b/dts/framework/params/eal.py
@@ -35,9 +35,9 @@ class EalParams(Params):
                 ``other_eal_param='--single-file-segments'``
     """
 
-    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
-    memory_channels: int = field(metadata=Params.short("n"))
-    prefix: str = field(metadata=Params.long("file-prefix"))
+    lcore_list: LogicalCoreList | None = field(default=None, metadata=Params.short("l"))
+    memory_channels: int | None = field(default=None, metadata=Params.short("n"))
+    prefix: str = field(default="dpdk", metadata=Params.long("file-prefix"))
     no_pci: Switch = None
     vdevs: list[VirtualDevice] | None = field(
         default=None, metadata=Params.multiple() | Params.long("vdev")
diff --git a/dts/framework/remote_session/dpdk_shell.py b/dts/framework/remote_session/dpdk_shell.py
new file mode 100644
index 0000000000..2cbf69ae9a
--- /dev/null
+++ b/dts/framework/remote_session/dpdk_shell.py
@@ -0,0 +1,106 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Base interactive shell for DPDK applications.
+
+Provides a base class to create interactive shells based on DPDK.
+"""
+
+
+from abc import ABC
+
+from framework.params.eal import EalParams
+from framework.remote_session.interactive_shell import InteractiveShell
+from framework.settings import SETTINGS
+from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
+from framework.testbed_model.sut_node import SutNode
+
+
+def compute_eal_params(
+    sut_node: SutNode,
+    params: EalParams | None = None,
+    lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
+    ascending_cores: bool = True,
+    append_prefix_timestamp: bool = True,
+) -> EalParams:
+    """Compute EAL parameters based on the node's specifications.
+
+    Args:
+        sut_node: The SUT node to compute the values for.
+        params: If set to None a new object is created and returned. Otherwise the given
+            :class:`EalParams`'s lcore_list is modified according to the given filter specifier.
+            A DPDK prefix is added. If ports is set to None, all the SUT node's ports are
+            automatically assigned.
+        lcore_filter_specifier: A number of lcores/cores/sockets to use or a list of lcore ids to
+            use. The default will select one lcore for each of two cores on one socket, in ascending
+            order of core ids.
+        ascending_cores: Sort cores in ascending order (lowest to highest IDs). If :data:`False`,
+            sort in descending order.
+        append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.
+    """
+    if params is None:
+        params = EalParams()
+
+    if params.lcore_list is None:
+        params.lcore_list = LogicalCoreList(
+            sut_node.filter_lcores(lcore_filter_specifier, ascending_cores)
+        )
+
+    prefix = params.prefix
+    if append_prefix_timestamp:
+        prefix = f"{prefix}_{sut_node.dpdk_timestamp}"
+    prefix = sut_node.main_session.get_dpdk_file_prefix(prefix)
+    if prefix:
+        sut_node.dpdk_prefix_list.append(prefix)
+    params.prefix = prefix
+
+    if params.ports is None:
+        params.ports = sut_node.ports
+
+    return params
+
+
+class DPDKShell(InteractiveShell, ABC):
+    """The base class for managing DPDK-based interactive shells.
+
+    This class shouldn't be instantiated directly, but instead be extended.
+    It automatically injects computed EAL parameters based on the node in the
+    supplied app parameters.
+    """
+
+    _node: SutNode
+    _app_params: EalParams
+
+    _lcore_filter_specifier: LogicalCoreCount | LogicalCoreList
+    _ascending_cores: bool
+    _append_prefix_timestamp: bool
+
+    def __init__(
+        self,
+        node: SutNode,
+        privileged: bool = True,
+        timeout: float = SETTINGS.timeout,
+        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
+        ascending_cores: bool = True,
+        append_prefix_timestamp: bool = True,
+        start_on_init: bool = True,
+        app_params: EalParams = EalParams(),
+    ) -> None:
+        """Overrides :meth:`~.interactive_shell.InteractiveShell.__init__`."""
+        self._lcore_filter_specifier = lcore_filter_specifier
+        self._ascending_cores = ascending_cores
+        self._append_prefix_timestamp = append_prefix_timestamp
+
+        super().__init__(node, privileged, timeout, start_on_init, app_params)
+
+    def _post_init(self):
+        """Computes EAL params based on the node capabilities before start."""
+        self._app_params = compute_eal_params(
+            self._node,
+            self._app_params,
+            self._lcore_filter_specifier,
+            self._ascending_cores,
+            self._append_prefix_timestamp,
+        )
+
+        self._update_path(self._node.remote_dpdk_build_dir.joinpath(self.path))
diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
index 8191b36630..5a8a6d6d15 100644
--- a/dts/framework/remote_session/interactive_shell.py
+++ b/dts/framework/remote_session/interactive_shell.py
@@ -17,13 +17,14 @@
 
 from abc import ABC
 from pathlib import PurePath
-from typing import Callable, ClassVar
+from typing import ClassVar
 
-from paramiko import Channel, SSHClient, channel  # type: ignore[import-untyped]
+from paramiko import Channel, channel  # type: ignore[import-untyped]
 
 from framework.logger import DTSLogger
 from framework.params import Params
 from framework.settings import SETTINGS
+from framework.testbed_model.node import Node
 
 
 class InteractiveShell(ABC):
@@ -36,13 +37,14 @@ class InteractiveShell(ABC):
     session.
     """
 
-    _interactive_session: SSHClient
+    _node: Node
     _stdin: channel.ChannelStdinFile
     _stdout: channel.ChannelFile
     _ssh_channel: Channel
     _logger: DTSLogger
     _timeout: float
     _app_params: Params
+    _privileged: bool
 
     #: Prompt to expect at the end of output when sending a command.
     #: This is often overridden by subclasses.
@@ -56,56 +58,63 @@ class InteractiveShell(ABC):
     #: Path to the executable to start the interactive application.
     path: ClassVar[PurePath]
 
-    #: Whether this application is a DPDK app. If it is, the build directory
-    #: for DPDK on the node will be prepended to the path to the executable.
-    dpdk_app: ClassVar[bool] = False
-
     def __init__(
         self,
-        interactive_session: SSHClient,
-        logger: DTSLogger,
-        get_privileged_command: Callable[[str], str] | None,
-        app_params: Params = Params(),
+        node: Node,
+        privileged: bool = False,
         timeout: float = SETTINGS.timeout,
+        start_on_init: bool = True,
+        app_params: Params = Params(),
     ) -> None:
         """Create an SSH channel during initialization.
 
         Args:
-            interactive_session: The SSH session dedicated to interactive shells.
-            logger: The logger instance this session will use.
-            get_privileged_command: A method for modifying a command to allow it to use
-                elevated privileges. If :data:`None`, the application will not be started
-                with elevated privileges.
-            app_params: The command line parameters to be passed to the application on startup.
+            node: The node on which to run start the interactive shell.
+            privileged: Enables the shell to run as superuser.
             timeout: The timeout used for the SSH channel that is dedicated to this interactive
                 shell. This timeout is for collecting output, so if reading from the buffer
                 and no output is gathered within the timeout, an exception is thrown.
+            start_on_init: Start interactive shell automatically after object initialisation.
+            app_params: The command line parameters to be passed to the application on startup.
         """
-        self._interactive_session = interactive_session
-        self._ssh_channel = self._interactive_session.invoke_shell()
+        self._node = node
+        self._logger = node._logger
+        self._app_params = app_params
+        self._privileged = privileged
+        self._timeout = timeout
+        # Ensure path is properly formatted for the host
+        self._update_path(self._node.main_session.join_remote_path(self.path))
+
+        self._post_init()
+
+        if start_on_init:
+            self.start_application()
+
+    def _post_init(self):
+        """Overridable. Method called after the object init and before application start."""
+
+    def _setup_ssh_channel(self):
+        self._ssh_channel = self._node.main_session.interactive_session.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.settimeout(self._timeout)
         self._ssh_channel.set_combine_stderr(True)  # combines stdout and stderr streams
-        self._logger = logger
-        self._timeout = timeout
-        self._app_params = app_params
-        self._start_application(get_privileged_command)
 
-    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
+    def _make_start_command(self) -> str:
+        """Makes the command that starts the interactive shell."""
+        start_command = f"{self.path} {self._app_params or ''}"
+        if self._privileged:
+            start_command = self._node.main_session._get_privileged_command(start_command)
+        return start_command
+
+    def start_application(self) -> None:
         """Starts a new interactive application based on the path to the app.
 
         This method is often overridden by subclasses as their process for
         starting may look different.
-
-        Args:
-            get_privileged_command: A function (but could be any callable) that produces
-                the version of the command with elevated privileges.
         """
-        start_command = f"{self.path} {self._app_params}"
-        if get_privileged_command is not None:
-            start_command = get_privileged_command(start_command)
-        self.send_command(start_command)
+        self._setup_ssh_channel()
+        self.send_command(self._make_start_command())
 
     def send_command(
         self, command: str, prompt: str | None = None, skip_first_line: bool = False
@@ -156,3 +165,7 @@ def close(self) -> None:
     def __del__(self) -> None:
         """Make sure the session is properly closed before deleting the object."""
         self.close()
+
+    @classmethod
+    def _update_path(cls, path: PurePath) -> None:
+        cls.path = path
diff --git a/dts/framework/remote_session/python_shell.py b/dts/framework/remote_session/python_shell.py
index ccfd3783e8..953ed100df 100644
--- a/dts/framework/remote_session/python_shell.py
+++ b/dts/framework/remote_session/python_shell.py
@@ -6,9 +6,7 @@
 Typical usage example in a TestSuite::
 
     from framework.remote_session import PythonShell
-    python_shell = self.tg_node.create_interactive_shell(
-        PythonShell, timeout=5, privileged=True
-    )
+    python_shell = PythonShell(self.tg_node, timeout=5, privileged=True)
     python_shell.send_command("print('Hello World')")
     python_shell.close()
 """
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index 82701a9839..8ee6829067 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -7,9 +7,7 @@
 
 Typical usage example in a TestSuite::
 
-    testpmd_shell = self.sut_node.create_interactive_shell(
-            TestPmdShell, privileged=True
-        )
+    testpmd_shell = TestPmdShell(self.sut_node)
     devices = testpmd_shell.get_devices()
     for device in devices:
         print(device)
@@ -21,18 +19,19 @@
 from dataclasses import dataclass, field
 from enum import Flag, auto
 from pathlib import PurePath
-from typing import Callable, ClassVar
+from typing import ClassVar
 
 from typing_extensions import Self
 
 from framework.exception import InteractiveCommandExecutionError
 from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
 from framework.parser import ParserFn, TextParser
+from framework.remote_session.dpdk_shell import DPDKShell
 from framework.settings import SETTINGS
+from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
+from framework.testbed_model.sut_node import SutNode
 from framework.utils import StrEnum
 
-from .interactive_shell import InteractiveShell
-
 
 class TestPmdDevice(object):
     """The data of a device that testpmd can recognize.
@@ -577,52 +576,48 @@ class TestPmdPortStats(TextParser):
     tx_bps: int = field(metadata=TextParser.find_int(r"Tx-bps:\s+(\d+)"))
 
 
-class TestPmdShell(InteractiveShell):
+class TestPmdShell(DPDKShell):
     """Testpmd interactive shell.
 
     The testpmd shell users should never use
     the :meth:`~.interactive_shell.InteractiveShell.send_command` method directly, but rather
     call specialized methods. If there isn't one that satisfies a need, it should be added.
-
-    Attributes:
-        number_of_ports: The number of ports which were allowed on the command-line when testpmd
-            was started.
     """
 
-    number_of_ports: int
+    _app_params: TestPmdParams
 
     #: The path to the testpmd executable.
     path: ClassVar[PurePath] = PurePath("app", "dpdk-testpmd")
 
-    #: Flag this as a DPDK app so that it's clear this is not a system app and
-    #: needs to be looked in a specific path.
-    dpdk_app: ClassVar[bool] = True
-
     #: The testpmd's prompt.
     _default_prompt: ClassVar[str] = "testpmd>"
 
     #: This forces the prompt to appear after sending a command.
     _command_extra_chars: ClassVar[str] = "\n"
 
-    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
-        """Overrides :meth:`~.interactive_shell._start_application`.
-
-        Add flags for starting testpmd in interactive mode and disabling messages for link state
-        change events before starting the application. Link state is verified before starting
-        packet forwarding and the messages create unexpected newlines in the terminal which
-        complicates output collection.
-
-        Also find the number of pci addresses which were allowed on the command line when the app
-        was started.
-        """
-        assert isinstance(self._app_params, TestPmdParams)
-
-        self.number_of_ports = (
-            len(self._app_params.ports) if self._app_params.ports is not None else 0
+    def __init__(
+        self,
+        node: SutNode,
+        privileged: bool = True,
+        timeout: float = SETTINGS.timeout,
+        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
+        ascending_cores: bool = True,
+        append_prefix_timestamp: bool = True,
+        start_on_init: bool = True,
+        **app_params,
+    ) -> None:
+        """Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs."""
+        super().__init__(
+            node,
+            privileged,
+            timeout,
+            lcore_filter_specifier,
+            ascending_cores,
+            append_prefix_timestamp,
+            start_on_init,
+            TestPmdParams(**app_params),
         )
 
-        super()._start_application(get_privileged_command)
-
     def start(self, verify: bool = True) -> None:
         """Start packet forwarding with the current configuration.
 
@@ -642,7 +637,8 @@ def start(self, verify: bool = True) -> None:
                 self._logger.debug(f"Failed to start packet forwarding: \n{start_cmd_output}")
                 raise InteractiveCommandExecutionError("Testpmd failed to start packet forwarding.")
 
-            for port_id in range(self.number_of_ports):
+            number_of_ports = len(self._app_params.ports or [])
+            for port_id in range(number_of_ports):
                 if not self.wait_link_status_up(port_id):
                     raise InteractiveCommandExecutionError(
                         "Not all ports came up after starting packet forwarding in testpmd."
diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
index 6af4f25a3c..88395faabe 100644
--- a/dts/framework/testbed_model/node.py
+++ b/dts/framework/testbed_model/node.py
@@ -15,7 +15,7 @@
 
 from abc import ABC
 from ipaddress import IPv4Interface, IPv6Interface
-from typing import Any, Callable, Type, Union
+from typing import Any, Callable, Union
 
 from framework.config import (
     OS,
@@ -25,7 +25,6 @@
 )
 from framework.exception import ConfigurationError
 from framework.logger import DTSLogger, get_dts_logger
-from framework.params import Params
 from framework.settings import SETTINGS
 
 from .cpu import (
@@ -36,7 +35,7 @@
     lcore_filter,
 )
 from .linux_session import LinuxSession
-from .os_session import InteractiveShellType, OSSession
+from .os_session import OSSession
 from .port import Port
 from .virtual_device import VirtualDevice
 
@@ -196,37 +195,6 @@ def create_session(self, name: str) -> OSSession:
         self._other_sessions.append(connection)
         return connection
 
-    def create_interactive_shell(
-        self,
-        shell_cls: Type[InteractiveShellType],
-        timeout: float = SETTINGS.timeout,
-        privileged: bool = False,
-        app_params: Params = Params(),
-    ) -> InteractiveShellType:
-        """Factory for interactive session handlers.
-
-        Instantiate `shell_cls` according to the remote OS specifics.
-
-        Args:
-            shell_cls: The class of the shell.
-            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.
-            privileged: Whether to run the shell with administrative privileges.
-            app_args: The arguments to be passed to the application.
-
-        Returns:
-            An instance of the desired interactive application shell.
-        """
-        if not shell_cls.dpdk_app:
-            shell_cls.path = self.main_session.join_remote_path(shell_cls.path)
-
-        return self.main_session.create_interactive_shell(
-            shell_cls,
-            timeout,
-            privileged,
-            app_params,
-        )
-
     def filter_lcores(
         self,
         filter_specifier: LogicalCoreCount | LogicalCoreList,
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
index e5f5fcbe0e..e7e6c9d670 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -26,18 +26,16 @@
 from collections.abc import Iterable
 from ipaddress import IPv4Interface, IPv6Interface
 from pathlib import PurePath
-from typing import Type, TypeVar, Union
+from typing import Union
 
 from framework.config import Architecture, NodeConfiguration, NodeInfo
 from framework.logger import DTSLogger
-from framework.params import Params
 from framework.remote_session import (
     InteractiveRemoteSession,
     RemoteSession,
     create_interactive_session,
     create_remote_session,
 )
-from framework.remote_session.interactive_shell import InteractiveShell
 from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
@@ -45,8 +43,6 @@
 from .cpu import LogicalCore
 from .port import Port
 
-InteractiveShellType = TypeVar("InteractiveShellType", bound=InteractiveShell)
-
 
 class OSSession(ABC):
     """OS-unaware to OS-aware translation API definition.
@@ -131,36 +127,6 @@ def send_command(
 
         return self.remote_session.send_command(command, timeout, verify, env)
 
-    def create_interactive_shell(
-        self,
-        shell_cls: Type[InteractiveShellType],
-        timeout: float,
-        privileged: bool,
-        app_args: Params,
-    ) -> InteractiveShellType:
-        """Factory for interactive session handlers.
-
-        Instantiate `shell_cls` according to the remote OS specifics.
-
-        Args:
-            shell_cls: The class of the shell.
-            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.
-            privileged: Whether to run the shell with administrative privileges.
-            app_args: The arguments to be passed to the application.
-
-        Returns:
-            An instance of the desired interactive application shell.
-        """
-        return shell_cls(
-            self.interactive_session.session,
-            self._logger,
-            self._get_privileged_command if privileged else None,
-            app_args,
-            timeout,
-        )
-
     @staticmethod
     @abstractmethod
     def _get_privileged_command(command: str) -> str:
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index 83ad06ae2d..d231a01425 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -16,7 +16,6 @@
 import tarfile
 import time
 from pathlib import PurePath
-from typing import Type
 
 from framework.config import (
     BuildTargetConfiguration,
@@ -24,17 +23,13 @@
     NodeInfo,
     SutNodeConfiguration,
 )
-from framework.params import Params, Switch
 from framework.params.eal import EalParams
 from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
 
-from .cpu import LogicalCoreCount, LogicalCoreList
 from .node import Node
-from .os_session import InteractiveShellType, OSSession
-from .port import Port
-from .virtual_device import VirtualDevice
+from .os_session import OSSession
 
 
 class SutNode(Node):
@@ -56,8 +51,8 @@ class SutNode(Node):
     """
 
     config: SutNodeConfiguration
-    _dpdk_prefix_list: list[str]
-    _dpdk_timestamp: str
+    dpdk_prefix_list: list[str]
+    dpdk_timestamp: str
     _build_target_config: BuildTargetConfiguration | None
     _env_vars: dict
     _remote_tmp_dir: PurePath
@@ -76,14 +71,14 @@ def __init__(self, node_config: SutNodeConfiguration):
             node_config: The SUT node's test run configuration.
         """
         super(SutNode, self).__init__(node_config)
-        self._dpdk_prefix_list = []
+        self.dpdk_prefix_list = []
         self._build_target_config = None
         self._env_vars = {}
         self._remote_tmp_dir = self.main_session.get_remote_tmp_dir()
         self.__remote_dpdk_dir = None
         self._app_compile_timeout = 90
         self._dpdk_kill_session = None
-        self._dpdk_timestamp = (
+        self.dpdk_timestamp = (
             f"{str(os.getpid())}_{time.strftime('%Y%m%d%H%M%S', time.localtime())}"
         )
         self._dpdk_version = None
@@ -283,73 +278,11 @@ def kill_cleanup_dpdk_apps(self) -> None:
         """Kill all dpdk applications on the SUT, then clean up hugepages."""
         if self._dpdk_kill_session and self._dpdk_kill_session.is_alive():
             # we can use the session if it exists and responds
-            self._dpdk_kill_session.kill_cleanup_dpdk_apps(self._dpdk_prefix_list)
+            self._dpdk_kill_session.kill_cleanup_dpdk_apps(self.dpdk_prefix_list)
         else:
             # otherwise, we need to (re)create it
             self._dpdk_kill_session = self.create_session("dpdk_kill")
-        self._dpdk_prefix_list = []
-
-    def create_eal_parameters(
-        self,
-        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
-        ascending_cores: bool = True,
-        prefix: str = "dpdk",
-        append_prefix_timestamp: bool = True,
-        no_pci: Switch = None,
-        vdevs: list[VirtualDevice] | None = None,
-        ports: list[Port] | None = None,
-        other_eal_param: str = "",
-    ) -> EalParams:
-        """Compose the EAL parameters.
-
-        Process the list of cores and the DPDK prefix and pass that along with
-        the rest of the arguments.
-
-        Args:
-            lcore_filter_specifier: A number of lcores/cores/sockets to use
-                or a list of lcore ids to use.
-                The default will select one lcore for each of two cores
-                on one socket, in ascending order of core ids.
-            ascending_cores: Sort cores in ascending order (lowest to highest IDs).
-                If :data:`False`, sort in descending order.
-            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
-            append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.
-            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
-            vdevs: Virtual devices, e.g.::
-
-                vdevs=[
-                    VirtualDevice('net_ring0'),
-                    VirtualDevice('net_ring1')
-                ]
-            ports: The list of ports to allow. If :data:`None`, all ports listed in `self.ports`
-                will be allowed.
-            other_eal_param: user defined DPDK EAL parameters, e.g.:
-                ``other_eal_param='--single-file-segments'``.
-
-        Returns:
-            An EAL param string, such as
-            ``-c 0xf -a 0000:88:00.0 --file-prefix=dpdk_1112_20190809143420``.
-        """
-        lcore_list = LogicalCoreList(self.filter_lcores(lcore_filter_specifier, ascending_cores))
-
-        if append_prefix_timestamp:
-            prefix = f"{prefix}_{self._dpdk_timestamp}"
-        prefix = self.main_session.get_dpdk_file_prefix(prefix)
-        if prefix:
-            self._dpdk_prefix_list.append(prefix)
-
-        if ports is None:
-            ports = self.ports
-
-        return EalParams(
-            lcore_list=lcore_list,
-            memory_channels=self.config.memory_channels,
-            prefix=prefix,
-            no_pci=no_pci,
-            vdevs=vdevs,
-            ports=ports,
-            other_eal_param=Params.from_str(other_eal_param),
-        )
+        self.dpdk_prefix_list = []
 
     def run_dpdk_app(
         self, app_path: PurePath, eal_params: EalParams, timeout: float = 30
@@ -379,49 +312,6 @@ def configure_ipv4_forwarding(self, enable: bool) -> None:
         """
         self.main_session.configure_ipv4_forwarding(enable)
 
-    def create_interactive_shell(
-        self,
-        shell_cls: Type[InteractiveShellType],
-        timeout: float = SETTINGS.timeout,
-        privileged: bool = False,
-        app_params: Params = Params(),
-        eal_params: EalParams | None = None,
-    ) -> InteractiveShellType:
-        """Extend the factory for interactive session handlers.
-
-        The extensions are SUT node specific:
-
-            * The default for `eal_parameters`,
-            * The interactive shell path `shell_cls.path` is prepended with path to the remote
-              DPDK build directory for DPDK apps.
-
-        Args:
-            shell_cls: The class of the shell.
-            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.
-            privileged: Whether to run the shell with administrative privileges.
-            app_params: The parameters to be passed to the application.
-            eal_params: List of EAL parameters to use to launch the app. If this
-                isn't provided or an empty string is passed, it will default to calling
-                :meth:`create_eal_parameters`.
-
-        Returns:
-            An instance of the desired interactive application shell.
-        """
-        # We need to append the build directory and add EAL parameters for DPDK apps
-        if shell_cls.dpdk_app:
-            if eal_params is None:
-                eal_params = self.create_eal_parameters()
-            eal_params.append_str(str(app_params))
-            app_params = eal_params
-
-            shell_cls.path = self.main_session.join_remote_path(
-                self.remote_dpdk_build_dir, shell_cls.path
-            )
-
-        return super().create_interactive_shell(shell_cls, timeout, privileged, app_params)
-
     def bind_ports_to_driver(self, for_dpdk: bool = True) -> None:
         """Bind all ports on the SUT to a driver.
 
diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py
index 7bc1c2cc08..bf58ad1c5e 100644
--- a/dts/framework/testbed_model/traffic_generator/scapy.py
+++ b/dts/framework/testbed_model/traffic_generator/scapy.py
@@ -217,9 +217,7 @@ def __init__(self, tg_node: Node, config: ScapyTrafficGeneratorConfig):
             self._tg_node.config.os == OS.linux
         ), "Linux is the only supported OS for scapy traffic generation"
 
-        self.session = self._tg_node.create_interactive_shell(
-            PythonShell, timeout=5, privileged=True
-        )
+        self.session = PythonShell(self._tg_node, timeout=5, privileged=True)
 
         # import libs in remote python console
         for import_statement in SCAPY_RPC_SERVER_IMPORTS:
diff --git a/dts/tests/TestSuite_hello_world.py b/dts/tests/TestSuite_hello_world.py
index 0d6995f260..d958f99030 100644
--- a/dts/tests/TestSuite_hello_world.py
+++ b/dts/tests/TestSuite_hello_world.py
@@ -7,6 +7,7 @@
 No other EAL parameters apart from cores are used.
 """
 
+from framework.remote_session.dpdk_shell import compute_eal_params
 from framework.test_suite import TestSuite
 from framework.testbed_model.cpu import (
     LogicalCoreCount,
@@ -38,7 +39,7 @@ def test_hello_world_single_core(self) -> None:
         # get the first usable core
         lcore_amount = LogicalCoreCount(1, 1, 1)
         lcores = LogicalCoreCountFilter(self.sut_node.lcores, lcore_amount).filter()
-        eal_para = self.sut_node.create_eal_parameters(lcore_filter_specifier=lcore_amount)
+        eal_para = compute_eal_params(self.sut_node, lcore_filter_specifier=lcore_amount)
         result = self.sut_node.run_dpdk_app(self.app_helloworld_path, eal_para)
         self.verify(
             f"hello from core {int(lcores[0])}" in result.stdout,
@@ -55,8 +56,8 @@ def test_hello_world_all_cores(self) -> None:
             "hello from core <core_id>"
         """
         # get the maximum logical core number
-        eal_para = self.sut_node.create_eal_parameters(
-            lcore_filter_specifier=LogicalCoreList(self.sut_node.lcores)
+        eal_para = compute_eal_params(
+            self.sut_node, lcore_filter_specifier=LogicalCoreList(self.sut_node.lcores)
         )
         result = self.sut_node.run_dpdk_app(self.app_helloworld_path, eal_para, 50)
         for lcore in self.sut_node.lcores:
diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index 6d206c1a40..43cf5c61eb 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -16,14 +16,13 @@
 """
 
 import struct
-from dataclasses import asdict
 
 from scapy.layers.inet import IP  # type: ignore[import-untyped]
 from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
 from scapy.packet import Raw  # type: ignore[import-untyped]
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
-from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
+from framework.params.testpmd import SimpleForwardingModes
 from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.test_suite import TestSuite
 
@@ -103,17 +102,13 @@ def pmd_scatter(self, mbsize: int) -> None:
         Test:
             Start testpmd and run functional test with preset mbsize.
         """
-        testpmd = self.sut_node.create_interactive_shell(
-            TestPmdShell,
-            app_params=TestPmdParams(
-                forward_mode=SimpleForwardingModes.mac,
-                mbcache=200,
-                mbuf_size=[mbsize],
-                max_pkt_len=9000,
-                tx_offloads=0x00008000,
-                **asdict(self.sut_node.create_eal_parameters()),
-            ),
-            privileged=True,
+        testpmd = TestPmdShell(
+            self.sut_node,
+            forward_mode=SimpleForwardingModes.mac,
+            mbcache=200,
+            mbuf_size=[mbsize],
+            max_pkt_len=9000,
+            tx_offloads=0x00008000,
         )
         testpmd.start()
 
diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py
index ca678f662d..eca27acfd8 100644
--- a/dts/tests/TestSuite_smoke_tests.py
+++ b/dts/tests/TestSuite_smoke_tests.py
@@ -99,7 +99,7 @@ def test_devices_listed_in_testpmd(self) -> None:
         Test:
             List all devices found in testpmd and verify the configured devices are among them.
         """
-        testpmd_driver = self.sut_node.create_interactive_shell(TestPmdShell, privileged=True)
+        testpmd_driver = TestPmdShell(self.sut_node)
         dev_list = [str(x) for x in testpmd_driver.get_devices()]
         for nic in self.nics_in_node:
             self.verify(
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v4 8/8] dts: use Unpack for type checking and hinting
  2024-06-17 14:42 ` [PATCH v4 0/8] dts: add testpmd params and statefulness Luca Vizzarro
                     ` (6 preceding siblings ...)
  2024-06-17 14:42   ` [PATCH v4 7/8] dts: rework interactive shells Luca Vizzarro
@ 2024-06-17 14:42   ` Luca Vizzarro
  7 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-17 14:42 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Interactive shells that inherit DPDKShell initialise their params
classes from a kwargs dict. Therefore, static type checking is
disabled. This change uses the functionality of Unpack added in
PEP 692 to re-enable it. The disadvantage is that this functionality has
been implemented only with TypedDict, forcing the creation of TypedDict
mirrors of the Params classes.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 dts/framework/params/types.py                 | 133 ++++++++++++++++++
 dts/framework/remote_session/testpmd_shell.py |   5 +-
 2 files changed, 136 insertions(+), 2 deletions(-)
 create mode 100644 dts/framework/params/types.py

diff --git a/dts/framework/params/types.py b/dts/framework/params/types.py
new file mode 100644
index 0000000000..e668f658d8
--- /dev/null
+++ b/dts/framework/params/types.py
@@ -0,0 +1,133 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Module containing TypeDict-equivalents of Params classes for static typing and hinting.
+
+TypedDicts can be used in conjunction with Unpack and kwargs for type hinting on function calls.
+
+Example:
+    ..code:: python
+        def create_testpmd(**kwargs: Unpack[TestPmdParamsDict]):
+            params = TestPmdParams(**kwargs)
+"""
+
+from pathlib import PurePath
+from typing import TypedDict
+
+from framework.params import Switch, YesNoSwitch
+from framework.params.testpmd import (
+    AnonMempoolAllocationMode,
+    EthPeer,
+    Event,
+    FlowGenForwardingMode,
+    HairpinMode,
+    NoisyForwardingMode,
+    Params,
+    PortNUMAConfig,
+    PortTopology,
+    RingNUMAConfig,
+    RSSSetting,
+    RXMultiQueueMode,
+    RXRingParams,
+    SimpleForwardingModes,
+    SimpleMempoolAllocationMode,
+    TxIPAddrPair,
+    TXOnlyForwardingMode,
+    TXRingParams,
+    TxUDPPortPair,
+)
+from framework.testbed_model.cpu import LogicalCoreList
+from framework.testbed_model.port import Port
+from framework.testbed_model.virtual_device import VirtualDevice
+
+
+class EalParamsDict(TypedDict, total=False):
+    """:class:`TypedDict` equivalent of :class:`~.eal.EalParams`."""
+
+    lcore_list: LogicalCoreList | None
+    memory_channels: int | None
+    prefix: str
+    no_pci: Switch
+    vdevs: list[VirtualDevice] | None
+    ports: list[Port] | None
+    other_eal_param: Params | None
+
+
+class TestPmdParamsDict(EalParamsDict, total=False):
+    """:class:`TypedDict` equivalent of :class:`~.testpmd.TestPmdParams`."""
+
+    interactive_mode: Switch
+    auto_start: Switch
+    tx_first: Switch
+    stats_period: int | None
+    display_xstats: list[str] | None
+    nb_cores: int | None
+    coremask: int | None
+    nb_ports: int | None
+    port_topology: PortTopology | None
+    portmask: int | None
+    portlist: str | None
+    numa: YesNoSwitch
+    socket_num: int | None
+    port_numa_config: list[PortNUMAConfig] | None
+    ring_numa_config: list[RingNUMAConfig] | None
+    total_num_mbufs: int | None
+    mbuf_size: list[int] | None
+    mbcache: int | None
+    max_pkt_len: int | None
+    eth_peers_configfile: PurePath | None
+    eth_peer: list[EthPeer] | None
+    tx_ip: TxIPAddrPair | None
+    tx_udp: TxUDPPortPair | None
+    enable_lro: Switch
+    max_lro_pkt_size: int | None
+    disable_crc_strip: Switch
+    enable_scatter: Switch
+    enable_hw_vlan: Switch
+    enable_hw_vlan_filter: Switch
+    enable_hw_vlan_strip: Switch
+    enable_hw_vlan_extend: Switch
+    enable_hw_qinq_strip: Switch
+    pkt_drop_enabled: Switch
+    rss: RSSSetting | None
+    forward_mode: (
+        SimpleForwardingModes
+        | FlowGenForwardingMode
+        | TXOnlyForwardingMode
+        | NoisyForwardingMode
+        | None
+    )
+    hairpin_mode: HairpinMode | None
+    hairpin_queues: int | None
+    burst: int | None
+    enable_rx_cksum: Switch
+    rx_queues: int | None
+    rx_ring: RXRingParams | None
+    no_flush_rx: Switch
+    rx_segments_offsets: list[int] | None
+    rx_segments_length: list[int] | None
+    multi_rx_mempool: Switch
+    rx_shared_queue: Switch | int
+    rx_offloads: int | None
+    rx_mq_mode: RXMultiQueueMode | None
+    tx_queues: int | None
+    tx_ring: TXRingParams | None
+    tx_offloads: int | None
+    eth_link_speed: int | None
+    disable_link_check: Switch
+    disable_device_start: Switch
+    no_lsc_interrupt: Switch
+    no_rmv_interrupt: Switch
+    bitrate_stats: int | None
+    latencystats: int | None
+    print_events: list[Event] | None
+    mask_events: list[Event] | None
+    flow_isolate_all: Switch
+    disable_flow_flush: Switch
+    hot_plug: Switch
+    vxlan_gpe_port: int | None
+    geneve_parsed_port: int | None
+    lock_all_memory: YesNoSwitch
+    mempool_allocation_mode: SimpleMempoolAllocationMode | AnonMempoolAllocationMode | None
+    record_core_cycles: Switch
+    record_burst_status: Switch
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index 8ee6829067..96a690b6de 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -21,10 +21,11 @@
 from pathlib import PurePath
 from typing import ClassVar
 
-from typing_extensions import Self
+from typing_extensions import Self, Unpack
 
 from framework.exception import InteractiveCommandExecutionError
 from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
+from framework.params.types import TestPmdParamsDict
 from framework.parser import ParserFn, TextParser
 from framework.remote_session.dpdk_shell import DPDKShell
 from framework.settings import SETTINGS
@@ -604,7 +605,7 @@ def __init__(
         ascending_cores: bool = True,
         append_prefix_timestamp: bool = True,
         start_on_init: bool = True,
-        **app_params,
+        **app_params: Unpack[TestPmdParamsDict],
     ) -> None:
         """Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs."""
         super().__init__(
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v5 0/8] dts: add testpmd params
  2024-03-26 19:04 [PATCH 0/6] dts: add testpmd params and statefulness Luca Vizzarro
                   ` (8 preceding siblings ...)
  2024-06-17 14:42 ` [PATCH v4 0/8] dts: add testpmd params and statefulness Luca Vizzarro
@ 2024-06-17 14:54 ` Luca Vizzarro
  2024-06-17 14:54   ` [PATCH v5 1/8] dts: add params manipulation module Luca Vizzarro
                     ` (7 more replies)
  2024-06-19 10:23 ` [PATCH v6 0/8] dts: add testpmd params Luca Vizzarro
  2024-06-19 14:02 ` [PATCH v7 0/8] dts: add testpmd params Luca Vizzarro
  11 siblings, 8 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-17 14:54 UTC (permalink / raw)
  To: dev; +Cc: Jeremy Spewock, Juraj Linkeš, Luca Vizzarro

v5:
- fixed typo
v4:
- fixed up docstrings
- made refactoring changes
- removed params value only
- rebased on top of show port info/stats
v3:
- refactored InteractiveShell methods
- fixed docstrings
v2:
- refactored the params module
- strengthened typing of the params module
- moved the params module into its own package
- refactored EalParams and TestPmdParams and
  moved under the params package
- reworked interactions between nodes and shells
- refactored imports leading to circular dependencies

---
Depends-on: series-32112 ("dts: testpmd show port info/stats")
---

Luca Vizzarro (8):
  dts: add params manipulation module
  dts: use Params for interactive shells
  dts: refactor EalParams
  dts: remove module-wide imports
  dts: add testpmd shell params
  dts: use testpmd params for scatter test suite
  dts: rework interactive shells
  dts: use Unpack for type checking and hinting

 dts/framework/params/__init__.py              | 358 +++++++++++
 dts/framework/params/eal.py                   |  50 ++
 dts/framework/params/testpmd.py               | 607 ++++++++++++++++++
 dts/framework/params/types.py                 | 133 ++++
 dts/framework/remote_session/__init__.py      |   7 +-
 dts/framework/remote_session/dpdk_shell.py    | 106 +++
 .../remote_session/interactive_shell.py       |  83 ++-
 dts/framework/remote_session/python_shell.py  |   4 +-
 dts/framework/remote_session/testpmd_shell.py |  99 +--
 dts/framework/runner.py                       |   4 +-
 dts/framework/test_suite.py                   |   5 +-
 dts/framework/testbed_model/__init__.py       |   9 -
 dts/framework/testbed_model/node.py           |  36 +-
 dts/framework/testbed_model/os_session.py     |  38 +-
 dts/framework/testbed_model/sut_node.py       | 194 +-----
 .../testbed_model/traffic_generator/scapy.py  |   6 +-
 dts/tests/TestSuite_hello_world.py            |   9 +-
 dts/tests/TestSuite_pmd_buffer_scatter.py     |  21 +-
 dts/tests/TestSuite_smoke_tests.py            |   4 +-
 19 files changed, 1384 insertions(+), 389 deletions(-)
 create mode 100644 dts/framework/params/__init__.py
 create mode 100644 dts/framework/params/eal.py
 create mode 100644 dts/framework/params/testpmd.py
 create mode 100644 dts/framework/params/types.py
 create mode 100644 dts/framework/remote_session/dpdk_shell.py

-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v5 1/8] dts: add params manipulation module
  2024-06-17 14:54 ` [PATCH v5 0/8] dts: add testpmd params Luca Vizzarro
@ 2024-06-17 14:54   ` Luca Vizzarro
  2024-06-17 15:22     ` Nicholas Pratte
  2024-06-17 14:54   ` [PATCH v5 2/8] dts: use Params for interactive shells Luca Vizzarro
                     ` (6 subsequent siblings)
  7 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-17 14:54 UTC (permalink / raw)
  To: dev; +Cc: Jeremy Spewock, Juraj Linkeš, Luca Vizzarro, Paul Szczepanek

This commit introduces a new "params" module, which adds a new way
to manage command line parameters. The provided Params dataclass
is able to read the fields of its child class and produce a string
representation to supply to the command line. Any data structure
that is intended to represent command line parameters can inherit it.

The main purpose is to make it easier to represent data structures that
map to parameters. Aiding quicker development, while minimising code
bloat.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 dts/framework/params/__init__.py | 358 +++++++++++++++++++++++++++++++
 1 file changed, 358 insertions(+)
 create mode 100644 dts/framework/params/__init__.py

diff --git a/dts/framework/params/__init__.py b/dts/framework/params/__init__.py
new file mode 100644
index 0000000000..107b070ed2
--- /dev/null
+++ b/dts/framework/params/__init__.py
@@ -0,0 +1,358 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Parameter manipulation module.
+
+This module provides :class:`Params` which can be used to model any data structure
+that is meant to represent any command line parameters.
+"""
+
+from dataclasses import dataclass, fields
+from enum import Flag
+from typing import (
+    Any,
+    Callable,
+    Iterable,
+    Literal,
+    Reversible,
+    TypedDict,
+    TypeVar,
+    cast,
+)
+
+from typing_extensions import Self
+
+T = TypeVar("T")
+
+#: Type for a function taking one argument.
+FnPtr = Callable[[Any], Any]
+#: Type for a switch parameter.
+Switch = Literal[True, None]
+#: Type for a yes/no switch parameter.
+YesNoSwitch = Literal[True, False, None]
+
+
+def _reduce_functions(funcs: Iterable[FnPtr]) -> FnPtr:
+    """Reduces an iterable of :attr:`FnPtr` from left to right to a single function.
+
+    If the iterable is empty, the created function just returns its fed value back.
+
+    Args:
+        funcs: An iterable containing the functions to be chained from left to right.
+
+    Returns:
+        FnPtr: A function that calls the given functions from left to right.
+    """
+
+    def reduced_fn(value):
+        for fn in funcs:
+            value = fn(value)
+        return value
+
+    return reduced_fn
+
+
+def modify_str(*funcs: FnPtr) -> Callable[[T], T]:
+    """Class decorator modifying the ``__str__`` method with a function created from its arguments.
+
+    The :attr:`FnPtr`s fed to the decorator are executed from left to right in the arguments list
+    order.
+
+    Args:
+        *funcs: The functions to chain from left to right.
+
+    Returns:
+        The decorator.
+
+    Example:
+        .. code:: python
+
+            @convert_str(hex_from_flag_value)
+            class BitMask(enum.Flag):
+                A = auto()
+                B = auto()
+
+        will allow ``BitMask`` to render as a hexadecimal value.
+    """
+
+    def _class_decorator(original_class):
+        original_class.__str__ = _reduce_functions(funcs)
+        return original_class
+
+    return _class_decorator
+
+
+def comma_separated(values: Iterable[Any]) -> str:
+    """Converts an iterable into a comma-separated string.
+
+    Args:
+        values: An iterable of objects.
+
+    Returns:
+        A comma-separated list of stringified values.
+    """
+    return ",".join([str(value).strip() for value in values if value is not None])
+
+
+def bracketed(value: str) -> str:
+    """Adds round brackets to the input.
+
+    Args:
+        value: Any string.
+
+    Returns:
+        A string surrounded by round brackets.
+    """
+    return f"({value})"
+
+
+def str_from_flag_value(flag: Flag) -> str:
+    """Returns the value from a :class:`enum.Flag` as a string.
+
+    Args:
+        flag: An instance of :class:`Flag`.
+
+    Returns:
+        The stringified value of the given flag.
+    """
+    return str(flag.value)
+
+
+def hex_from_flag_value(flag: Flag) -> str:
+    """Returns the value from a :class:`enum.Flag` converted to hexadecimal.
+
+    Args:
+        flag: An instance of :class:`Flag`.
+
+    Returns:
+        The value of the given flag in hexadecimal representation.
+    """
+    return hex(flag.value)
+
+
+class ParamsModifier(TypedDict, total=False):
+    """Params modifiers dict compatible with the :func:`dataclasses.field` metadata parameter."""
+
+    #:
+    Params_short: str
+    #:
+    Params_long: str
+    #:
+    Params_multiple: bool
+    #:
+    Params_convert_value: Reversible[FnPtr]
+
+
+@dataclass
+class Params:
+    """Dataclass that renders its fields into command line arguments.
+
+    The parameter name is taken from the field name by default. The following:
+
+    .. code:: python
+
+        name: str | None = "value"
+
+    is rendered as ``--name=value``.
+    Through :func:`dataclasses.field` the resulting parameter can be manipulated by applying
+    this class' metadata modifier functions. These return regular dictionaries which can be combined
+    together using the pipe (OR) operator.
+
+    To use fields as switches, set the value to ``True`` to render them. If you
+    use a yes/no switch you can also set ``False`` which would render a switch
+    prefixed with ``--no-``. Examples:
+
+    .. code:: python
+
+        interactive: Switch = True  # renders --interactive
+        numa: YesNoSwitch   = False # renders --no-numa
+
+    Setting ``None`` will prevent it from being rendered. The :attr:`~Switch` type alias is provided
+    for regular switches, whereas :attr:`~YesNoSwitch` is offered for yes/no ones.
+
+    An instance of a dataclass inheriting ``Params`` can also be assigned to an attribute,
+    this helps with grouping parameters together.
+    The attribute holding the dataclass will be ignored and the latter will just be rendered as
+    expected.
+    """
+
+    _suffix = ""
+    """Holder of the plain text value of Params when called directly. A suffix for child classes."""
+
+    """========= BEGIN FIELD METADATA MODIFIER FUNCTIONS ========"""
+
+    @staticmethod
+    def short(name: str) -> ParamsModifier:
+        """Overrides any parameter name with the given short option.
+
+        Args:
+            name: The short parameter name.
+
+        Returns:
+            ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
+                the parameter short name modifier.
+
+        Example:
+            .. code:: python
+
+                logical_cores: str | None = field(default="1-4", metadata=Params.short("l"))
+
+            will render as ``-l=1-4`` instead of ``--logical-cores=1-4``.
+        """
+        return ParamsModifier(Params_short=name)
+
+    @staticmethod
+    def long(name: str) -> ParamsModifier:
+        """Overrides the inferred parameter name to the specified one.
+
+        Args:
+            name: The long parameter name.
+
+        Returns:
+            ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
+                the parameter long name modifier.
+
+        Example:
+            .. code:: python
+
+                x_name: str | None = field(default="y", metadata=Params.long("x"))
+
+            will render as ``--x=y``, but the field is accessed and modified through ``x_name``.
+        """
+        return ParamsModifier(Params_long=name)
+
+    @staticmethod
+    def multiple() -> ParamsModifier:
+        """Specifies that this parameter is set multiple times. The parameter type must be a list.
+
+        Returns:
+            ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
+                the multiple parameters modifier.
+
+        Example:
+            .. code:: python
+
+                ports: list[int] | None = field(
+                    default_factory=lambda: [0, 1, 2],
+                    metadata=Params.multiple() | Params.long("port")
+                )
+
+            will render as ``--port=0 --port=1 --port=2``.
+        """
+        return ParamsModifier(Params_multiple=True)
+
+    @staticmethod
+    def convert_value(*funcs: FnPtr) -> ParamsModifier:
+        """Takes in a variable number of functions to convert the value text representation.
+
+        Functions can be chained together, executed from left to right in the arguments list order.
+
+        Args:
+            *funcs: The functions to chain from left to right.
+
+        Returns:
+            ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
+                the convert value modifier.
+
+        Example:
+            .. code:: python
+
+                hex_bitmask: int | None = field(
+                    default=0b1101,
+                    metadata=Params.convert_value(hex) | Params.long("mask")
+                )
+
+            will render as ``--mask=0xd``.
+        """
+        return ParamsModifier(Params_convert_value=funcs)
+
+    """========= END FIELD METADATA MODIFIER FUNCTIONS ========"""
+
+    def append_str(self, text: str) -> None:
+        """Appends a string at the end of the string representation.
+
+        Args:
+            text: Any text to append at the end of the parameters string representation.
+        """
+        self._suffix += text
+
+    def __iadd__(self, text: str) -> Self:
+        """Appends a string at the end of the string representation.
+
+        Args:
+            text: Any text to append at the end of the parameters string representation.
+
+        Returns:
+            The given instance back.
+        """
+        self.append_str(text)
+        return self
+
+    @classmethod
+    def from_str(cls, text: str) -> Self:
+        """Creates a plain Params object from a string.
+
+        Args:
+            text: The string parameters.
+
+        Returns:
+            A new plain instance of :class:`Params`.
+        """
+        obj = cls()
+        obj.append_str(text)
+        return obj
+
+    @staticmethod
+    def _make_switch(
+        name: str, is_short: bool = False, is_no: bool = False, value: str | None = None
+    ) -> str:
+        """Make the string representation of the parameter.
+
+        Args:
+            name: The name of the parameters.
+            is_short: If the parameters is short or not.
+            is_no: If the parameter is negated or not.
+            value: The value of the parameter.
+
+        Returns:
+            The complete command line parameter.
+        """
+        prefix = f"{'-' if is_short else '--'}{'no-' if is_no else ''}"
+        name = name.replace("_", "-")
+        value = f"{' ' if is_short else '='}{value}" if value else ""
+        return f"{prefix}{name}{value}"
+
+    def __str__(self) -> str:
+        """Returns a string of command-line-ready arguments from the class fields."""
+        arguments: list[str] = []
+
+        for field in fields(self):
+            value = getattr(self, field.name)
+            modifiers = cast(ParamsModifier, field.metadata)
+
+            if value is None:
+                continue
+
+            if isinstance(value, Params):
+                arguments.append(str(value))
+                continue
+
+            # take the short modifier, or the long modifier, or infer from field name
+            switch_name = modifiers.get("Params_short", modifiers.get("Params_long", field.name))
+            is_short = "Params_short" in modifiers
+
+            if isinstance(value, bool):
+                arguments.append(self._make_switch(switch_name, is_short, is_no=(not value)))
+                continue
+
+            convert = _reduce_functions(modifiers.get("Params_convert_value", []))
+            multiple = modifiers.get("Params_multiple", False)
+
+            values = value if multiple else [value]
+            for value in values:
+                arguments.append(self._make_switch(switch_name, is_short, value=convert(value)))
+
+        if self._suffix:
+            arguments.append(self._suffix)
+
+        return " ".join(arguments)
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v5 2/8] dts: use Params for interactive shells
  2024-06-17 14:54 ` [PATCH v5 0/8] dts: add testpmd params Luca Vizzarro
  2024-06-17 14:54   ` [PATCH v5 1/8] dts: add params manipulation module Luca Vizzarro
@ 2024-06-17 14:54   ` Luca Vizzarro
  2024-06-17 15:23     ` Nicholas Pratte
  2024-06-17 14:54   ` [PATCH v5 3/8] dts: refactor EalParams Luca Vizzarro
                     ` (5 subsequent siblings)
  7 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-17 14:54 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Make it so that interactive shells accept an implementation of `Params`
for app arguments. Convert EalParameters to use `Params` instead.

String command line parameters can still be supplied by using the
`Params.from_str()` method.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 .../remote_session/interactive_shell.py       |  12 +-
 dts/framework/remote_session/testpmd_shell.py |  11 +-
 dts/framework/testbed_model/node.py           |   6 +-
 dts/framework/testbed_model/os_session.py     |   4 +-
 dts/framework/testbed_model/sut_node.py       | 124 ++++++++----------
 dts/tests/TestSuite_pmd_buffer_scatter.py     |   3 +-
 6 files changed, 77 insertions(+), 83 deletions(-)

diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
index c025c52ba3..8191b36630 100644
--- a/dts/framework/remote_session/interactive_shell.py
+++ b/dts/framework/remote_session/interactive_shell.py
@@ -1,5 +1,6 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright(c) 2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """Common functionality for interactive shell handling.
 
@@ -21,6 +22,7 @@
 from paramiko import Channel, SSHClient, channel  # type: ignore[import-untyped]
 
 from framework.logger import DTSLogger
+from framework.params import Params
 from framework.settings import SETTINGS
 
 
@@ -40,7 +42,7 @@ class InteractiveShell(ABC):
     _ssh_channel: Channel
     _logger: DTSLogger
     _timeout: float
-    _app_args: str
+    _app_params: Params
 
     #: Prompt to expect at the end of output when sending a command.
     #: This is often overridden by subclasses.
@@ -63,7 +65,7 @@ def __init__(
         interactive_session: SSHClient,
         logger: DTSLogger,
         get_privileged_command: Callable[[str], str] | None,
-        app_args: str = "",
+        app_params: Params = Params(),
         timeout: float = SETTINGS.timeout,
     ) -> None:
         """Create an SSH channel during initialization.
@@ -74,7 +76,7 @@ def __init__(
             get_privileged_command: A method for modifying a command to allow it to use
                 elevated privileges. If :data:`None`, the application will not be started
                 with elevated privileges.
-            app_args: The command line arguments to be passed to the application on startup.
+            app_params: The command line parameters to be passed to the application on startup.
             timeout: The timeout used for the SSH channel that is dedicated to this interactive
                 shell. This timeout is for collecting output, so if reading from the buffer
                 and no output is gathered within the timeout, an exception is thrown.
@@ -87,7 +89,7 @@ def __init__(
         self._ssh_channel.set_combine_stderr(True)  # combines stdout and stderr streams
         self._logger = logger
         self._timeout = timeout
-        self._app_args = app_args
+        self._app_params = app_params
         self._start_application(get_privileged_command)
 
     def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
@@ -100,7 +102,7 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
             get_privileged_command: A function (but could be any callable) that produces
                 the version of the command with elevated privileges.
         """
-        start_command = f"{self.path} {self._app_args}"
+        start_command = f"{self.path} {self._app_params}"
         if get_privileged_command is not None:
             start_command = get_privileged_command(start_command)
         self.send_command(start_command)
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index d413bf2cc7..2836ed5c48 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -28,6 +28,7 @@
 from framework.exception import InteractiveCommandExecutionError
 from framework.parser import ParserFn, TextParser
 from framework.settings import SETTINGS
+from framework.testbed_model.sut_node import EalParams
 from framework.utils import StrEnum
 
 from .interactive_shell import InteractiveShell
@@ -645,8 +646,14 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
         Also find the number of pci addresses which were allowed on the command line when the app
         was started.
         """
-        self._app_args += " -i --mask-event intr_lsc"
-        self.number_of_ports = self._app_args.count("-a ")
+        self._app_params += " -i --mask-event intr_lsc"
+
+        assert isinstance(self._app_params, EalParams)
+
+        self.number_of_ports = (
+            len(self._app_params.ports) if self._app_params.ports is not None else 0
+        )
+
         super()._start_application(get_privileged_command)
 
     def start(self, verify: bool = True) -> None:
diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
index 74061f6262..6af4f25a3c 100644
--- a/dts/framework/testbed_model/node.py
+++ b/dts/framework/testbed_model/node.py
@@ -2,6 +2,7 @@
 # Copyright(c) 2010-2014 Intel Corporation
 # Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2022-2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """Common functionality for node management.
 
@@ -24,6 +25,7 @@
 )
 from framework.exception import ConfigurationError
 from framework.logger import DTSLogger, get_dts_logger
+from framework.params import Params
 from framework.settings import SETTINGS
 
 from .cpu import (
@@ -199,7 +201,7 @@ def create_interactive_shell(
         shell_cls: Type[InteractiveShellType],
         timeout: float = SETTINGS.timeout,
         privileged: bool = False,
-        app_args: str = "",
+        app_params: Params = Params(),
     ) -> InteractiveShellType:
         """Factory for interactive session handlers.
 
@@ -222,7 +224,7 @@ def create_interactive_shell(
             shell_cls,
             timeout,
             privileged,
-            app_args,
+            app_params,
         )
 
     def filter_lcores(
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
index d5bf7e0401..1a77aee532 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright(c) 2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """OS-aware remote session.
 
@@ -29,6 +30,7 @@
 
 from framework.config import Architecture, NodeConfiguration, NodeInfo
 from framework.logger import DTSLogger
+from framework.params import Params
 from framework.remote_session import (
     CommandResult,
     InteractiveRemoteSession,
@@ -134,7 +136,7 @@ def create_interactive_shell(
         shell_cls: Type[InteractiveShellType],
         timeout: float,
         privileged: bool,
-        app_args: str,
+        app_args: Params,
     ) -> InteractiveShellType:
         """Factory for interactive session handlers.
 
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index 97aa26d419..c886590979 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -2,6 +2,7 @@
 # Copyright(c) 2010-2014 Intel Corporation
 # Copyright(c) 2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """System under test (DPDK + hardware) node.
 
@@ -14,8 +15,9 @@
 import os
 import tarfile
 import time
+from dataclasses import dataclass, field
 from pathlib import PurePath
-from typing import Type
+from typing import Literal, Type
 
 from framework.config import (
     BuildTargetConfiguration,
@@ -23,6 +25,7 @@
     NodeInfo,
     SutNodeConfiguration,
 )
+from framework.params import Params, Switch
 from framework.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
@@ -34,62 +37,42 @@
 from .virtual_device import VirtualDevice
 
 
-class EalParameters(object):
-    """The environment abstraction layer parameters.
-
-    The string representation can be created by converting the instance to a string.
-    """
+def _port_to_pci(port: Port) -> str:
+    return port.pci
 
-    def __init__(
-        self,
-        lcore_list: LogicalCoreList,
-        memory_channels: int,
-        prefix: str,
-        no_pci: bool,
-        vdevs: list[VirtualDevice],
-        ports: list[Port],
-        other_eal_param: str,
-    ):
-        """Initialize the parameters according to inputs.
-
-        Process the parameters into the format used on the command line.
 
-        Args:
-            lcore_list: The list of logical cores to use.
-            memory_channels: The number of memory channels to use.
-            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
-            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
-            vdevs: Virtual devices, e.g.::
+@dataclass(kw_only=True)
+class EalParams(Params):
+    """The environment abstraction layer parameters.
 
-                vdevs=[
-                    VirtualDevice('net_ring0'),
-                    VirtualDevice('net_ring1')
-                ]
-            ports: The list of ports to allow.
-            other_eal_param: user defined DPDK EAL parameters, e.g.:
+    Attributes:
+        lcore_list: The list of logical cores to use.
+        memory_channels: The number of memory channels to use.
+        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
+        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
+        vdevs: Virtual devices, e.g.::
+            vdevs=[
+                VirtualDevice('net_ring0'),
+                VirtualDevice('net_ring1')
+            ]
+        ports: The list of ports to allow.
+        other_eal_param: user defined DPDK EAL parameters, e.g.:
                 ``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._ports = " ".join(f"-a {port.pci}" for port in ports)
-        self._other_eal_param = other_eal_param
-
-    def __str__(self) -> str:
-        """Create the EAL string."""
-        return (
-            f"{self._lcore_list} "
-            f"{self._memory_channels} "
-            f"{self._prefix} "
-            f"{self._no_pci} "
-            f"{self._vdevs} "
-            f"{self._ports} "
-            f"{self._other_eal_param}"
-        )
+    """
+
+    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
+    memory_channels: int = field(metadata=Params.short("n"))
+    prefix: str = field(metadata=Params.long("file-prefix"))
+    no_pci: Switch
+    vdevs: list[VirtualDevice] | None = field(
+        default=None, metadata=Params.multiple() | Params.long("vdev")
+    )
+    ports: list[Port] | None = field(
+        default=None,
+        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
+    )
+    other_eal_param: Params | None = None
+    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
 
 
 class SutNode(Node):
@@ -350,11 +333,11 @@ def create_eal_parameters(
         ascending_cores: bool = True,
         prefix: str = "dpdk",
         append_prefix_timestamp: bool = True,
-        no_pci: bool = False,
+        no_pci: Switch = None,
         vdevs: list[VirtualDevice] | None = None,
         ports: list[Port] | None = None,
         other_eal_param: str = "",
-    ) -> "EalParameters":
+    ) -> EalParams:
         """Compose the EAL parameters.
 
         Process the list of cores and the DPDK prefix and pass that along with
@@ -393,24 +376,21 @@ def create_eal_parameters(
         if prefix:
             self._dpdk_prefix_list.append(prefix)
 
-        if vdevs is None:
-            vdevs = []
-
         if ports is None:
             ports = self.ports
 
-        return EalParameters(
+        return EalParams(
             lcore_list=lcore_list,
             memory_channels=self.config.memory_channels,
             prefix=prefix,
             no_pci=no_pci,
             vdevs=vdevs,
             ports=ports,
-            other_eal_param=other_eal_param,
+            other_eal_param=Params.from_str(other_eal_param),
         )
 
     def run_dpdk_app(
-        self, app_path: PurePath, eal_args: "EalParameters", timeout: float = 30
+        self, app_path: PurePath, eal_params: EalParams, timeout: float = 30
     ) -> CommandResult:
         """Run DPDK application on the remote node.
 
@@ -419,14 +399,14 @@ def run_dpdk_app(
 
         Args:
             app_path: The remote path to the DPDK application.
-            eal_args: EAL parameters to run the DPDK application with.
+            eal_params: EAL parameters to run the DPDK application with.
             timeout: Wait at most this long in seconds for `command` execution to complete.
 
         Returns:
             The result of the DPDK app execution.
         """
         return self.main_session.send_command(
-            f"{app_path} {eal_args}", timeout, privileged=True, verify=True
+            f"{app_path} {eal_params}", timeout, privileged=True, verify=True
         )
 
     def configure_ipv4_forwarding(self, enable: bool) -> None:
@@ -442,8 +422,8 @@ def create_interactive_shell(
         shell_cls: Type[InteractiveShellType],
         timeout: float = SETTINGS.timeout,
         privileged: bool = False,
-        app_parameters: str = "",
-        eal_parameters: EalParameters | None = None,
+        app_params: Params = Params(),
+        eal_params: EalParams | None = None,
     ) -> InteractiveShellType:
         """Extend the factory for interactive session handlers.
 
@@ -459,26 +439,26 @@ def create_interactive_shell(
                 reading from the buffer and don't receive any data within the timeout
                 it will throw an error.
             privileged: Whether to run the shell with administrative privileges.
-            eal_parameters: List of EAL parameters to use to launch the app. If this
+            app_params: The parameters to be passed to the application.
+            eal_params: List of EAL parameters to use to launch the app. If this
                 isn't provided or an empty string is passed, it will default to calling
                 :meth:`create_eal_parameters`.
-            app_parameters: Additional arguments to pass into the application on the
-                command-line.
 
         Returns:
             An instance of the desired interactive application shell.
         """
         # We need to append the build directory and add EAL parameters for DPDK apps
         if shell_cls.dpdk_app:
-            if not eal_parameters:
-                eal_parameters = self.create_eal_parameters()
-            app_parameters = f"{eal_parameters} -- {app_parameters}"
+            if eal_params is None:
+                eal_params = self.create_eal_parameters()
+            eal_params.append_str(str(app_params))
+            app_params = eal_params
 
             shell_cls.path = self.main_session.join_remote_path(
                 self.remote_dpdk_build_dir, shell_cls.path
             )
 
-        return super().create_interactive_shell(shell_cls, timeout, privileged, app_parameters)
+        return super().create_interactive_shell(shell_cls, timeout, privileged, app_params)
 
     def bind_ports_to_driver(self, for_dpdk: bool = True) -> None:
         """Bind all ports on the SUT to a driver.
diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index a020682e8d..c6e93839cb 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -22,6 +22,7 @@
 from scapy.packet import Raw  # type: ignore[import-untyped]
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
+from framework.params import Params
 from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
 from framework.test_suite import TestSuite
 
@@ -103,7 +104,7 @@ def pmd_scatter(self, mbsize: int) -> None:
         """
         testpmd = self.sut_node.create_interactive_shell(
             TestPmdShell,
-            app_parameters=(
+            app_params=Params.from_str(
                 "--mbcache=200 "
                 f"--mbuf-size={mbsize} "
                 "--max-pkt-len=9000 "
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v5 3/8] dts: refactor EalParams
  2024-06-17 14:54 ` [PATCH v5 0/8] dts: add testpmd params Luca Vizzarro
  2024-06-17 14:54   ` [PATCH v5 1/8] dts: add params manipulation module Luca Vizzarro
  2024-06-17 14:54   ` [PATCH v5 2/8] dts: use Params for interactive shells Luca Vizzarro
@ 2024-06-17 14:54   ` Luca Vizzarro
  2024-06-17 15:23     ` Nicholas Pratte
  2024-06-17 14:54   ` [PATCH v5 4/8] dts: remove module-wide imports Luca Vizzarro
                     ` (4 subsequent siblings)
  7 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-17 14:54 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Move EalParams to its own module to avoid circular dependencies.
Also the majority of the attributes are now optional.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 dts/framework/params/eal.py                   | 50 +++++++++++++++++++
 dts/framework/remote_session/testpmd_shell.py |  2 +-
 dts/framework/testbed_model/sut_node.py       | 42 +---------------
 3 files changed, 53 insertions(+), 41 deletions(-)
 create mode 100644 dts/framework/params/eal.py

diff --git a/dts/framework/params/eal.py b/dts/framework/params/eal.py
new file mode 100644
index 0000000000..bbdbc8f334
--- /dev/null
+++ b/dts/framework/params/eal.py
@@ -0,0 +1,50 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Module representing the DPDK EAL-related parameters."""
+
+from dataclasses import dataclass, field
+from typing import Literal
+
+from framework.params import Params, Switch
+from framework.testbed_model.cpu import LogicalCoreList
+from framework.testbed_model.port import Port
+from framework.testbed_model.virtual_device import VirtualDevice
+
+
+def _port_to_pci(port: Port) -> str:
+    return port.pci
+
+
+@dataclass(kw_only=True)
+class EalParams(Params):
+    """The environment abstraction layer parameters.
+
+    Attributes:
+        lcore_list: The list of logical cores to use.
+        memory_channels: The number of memory channels to use.
+        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
+        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
+        vdevs: Virtual devices, e.g.::
+            vdevs=[
+                VirtualDevice('net_ring0'),
+                VirtualDevice('net_ring1')
+            ]
+        ports: The list of ports to allow.
+        other_eal_param: user defined DPDK EAL parameters, e.g.:
+                ``other_eal_param='--single-file-segments'``
+    """
+
+    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
+    memory_channels: int = field(metadata=Params.short("n"))
+    prefix: str = field(metadata=Params.long("file-prefix"))
+    no_pci: Switch = None
+    vdevs: list[VirtualDevice] | None = field(
+        default=None, metadata=Params.multiple() | Params.long("vdev")
+    )
+    ports: list[Port] | None = field(
+        default=None,
+        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
+    )
+    other_eal_param: Params | None = None
+    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index 2836ed5c48..2b9ef9418d 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -26,9 +26,9 @@
 from typing_extensions import Self
 
 from framework.exception import InteractiveCommandExecutionError
+from framework.params.eal import EalParams
 from framework.parser import ParserFn, TextParser
 from framework.settings import SETTINGS
-from framework.testbed_model.sut_node import EalParams
 from framework.utils import StrEnum
 
 from .interactive_shell import InteractiveShell
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index c886590979..e1163106a3 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -15,9 +15,8 @@
 import os
 import tarfile
 import time
-from dataclasses import dataclass, field
 from pathlib import PurePath
-from typing import Literal, Type
+from typing import Type
 
 from framework.config import (
     BuildTargetConfiguration,
@@ -26,6 +25,7 @@
     SutNodeConfiguration,
 )
 from framework.params import Params, Switch
+from framework.params.eal import EalParams
 from framework.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
@@ -37,44 +37,6 @@
 from .virtual_device import VirtualDevice
 
 
-def _port_to_pci(port: Port) -> str:
-    return port.pci
-
-
-@dataclass(kw_only=True)
-class EalParams(Params):
-    """The environment abstraction layer parameters.
-
-    Attributes:
-        lcore_list: The list of logical cores to use.
-        memory_channels: The number of memory channels to use.
-        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
-        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
-        vdevs: Virtual devices, e.g.::
-            vdevs=[
-                VirtualDevice('net_ring0'),
-                VirtualDevice('net_ring1')
-            ]
-        ports: The list of ports to allow.
-        other_eal_param: user defined DPDK EAL parameters, e.g.:
-                ``other_eal_param='--single-file-segments'``
-    """
-
-    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
-    memory_channels: int = field(metadata=Params.short("n"))
-    prefix: str = field(metadata=Params.long("file-prefix"))
-    no_pci: Switch
-    vdevs: list[VirtualDevice] | None = field(
-        default=None, metadata=Params.multiple() | Params.long("vdev")
-    )
-    ports: list[Port] | None = field(
-        default=None,
-        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
-    )
-    other_eal_param: Params | None = None
-    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
-
-
 class SutNode(Node):
     """The system under test node.
 
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v5 4/8] dts: remove module-wide imports
  2024-06-17 14:54 ` [PATCH v5 0/8] dts: add testpmd params Luca Vizzarro
                     ` (2 preceding siblings ...)
  2024-06-17 14:54   ` [PATCH v5 3/8] dts: refactor EalParams Luca Vizzarro
@ 2024-06-17 14:54   ` Luca Vizzarro
  2024-06-17 15:23     ` Nicholas Pratte
  2024-06-17 14:54   ` [PATCH v5 5/8] dts: add testpmd shell params Luca Vizzarro
                     ` (3 subsequent siblings)
  7 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-17 14:54 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Remove the imports in the testbed_model and remote_session modules init
file, to avoid the initialisation of unneeded modules, thus removing or
limiting the risk of circular dependencies.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 dts/framework/remote_session/__init__.py               | 7 +------
 dts/framework/runner.py                                | 4 +++-
 dts/framework/test_suite.py                            | 5 ++++-
 dts/framework/testbed_model/__init__.py                | 9 ---------
 dts/framework/testbed_model/os_session.py              | 4 ++--
 dts/framework/testbed_model/sut_node.py                | 2 +-
 dts/framework/testbed_model/traffic_generator/scapy.py | 2 +-
 dts/tests/TestSuite_hello_world.py                     | 2 +-
 dts/tests/TestSuite_smoke_tests.py                     | 2 +-
 9 files changed, 14 insertions(+), 23 deletions(-)

diff --git a/dts/framework/remote_session/__init__.py b/dts/framework/remote_session/__init__.py
index 1910c81c3c..0668e9c884 100644
--- a/dts/framework/remote_session/__init__.py
+++ b/dts/framework/remote_session/__init__.py
@@ -12,17 +12,12 @@
 allowing it to send and receive data within that particular shell.
 """
 
-# pylama:ignore=W0611
-
 from framework.config import NodeConfiguration
 from framework.logger import DTSLogger
 
 from .interactive_remote_session import InteractiveRemoteSession
-from .interactive_shell import InteractiveShell
-from .python_shell import PythonShell
-from .remote_session import CommandResult, RemoteSession
+from .remote_session import RemoteSession
 from .ssh_session import SSHSession
-from .testpmd_shell import TestPmdShell
 
 
 def create_remote_session(
diff --git a/dts/framework/runner.py b/dts/framework/runner.py
index dfdee14802..687bc04f79 100644
--- a/dts/framework/runner.py
+++ b/dts/framework/runner.py
@@ -26,6 +26,9 @@
 from types import FunctionType
 from typing import Iterable, Sequence
 
+from framework.testbed_model.sut_node import SutNode
+from framework.testbed_model.tg_node import TGNode
+
 from .config import (
     BuildTargetConfiguration,
     Configuration,
@@ -51,7 +54,6 @@
     TestSuiteWithCases,
 )
 from .test_suite import TestSuite
-from .testbed_model import SutNode, TGNode
 
 
 class DTSRunner:
diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py
index 8768f756a6..9d3debb00f 100644
--- a/dts/framework/test_suite.py
+++ b/dts/framework/test_suite.py
@@ -20,9 +20,12 @@
 from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
 from scapy.packet import Packet, Padding  # type: ignore[import-untyped]
 
+from framework.testbed_model.port import Port, PortLink
+from framework.testbed_model.sut_node import SutNode
+from framework.testbed_model.tg_node import TGNode
+
 from .exception import TestCaseVerifyError
 from .logger import DTSLogger, get_dts_logger
-from .testbed_model import Port, PortLink, SutNode, TGNode
 from .testbed_model.traffic_generator import PacketFilteringConfig
 from .utils import get_packet_summaries
 
diff --git a/dts/framework/testbed_model/__init__.py b/dts/framework/testbed_model/__init__.py
index 6086512ca2..e3edd4d811 100644
--- a/dts/framework/testbed_model/__init__.py
+++ b/dts/framework/testbed_model/__init__.py
@@ -17,12 +17,3 @@
 DTS needs to be able to connect to nodes and understand some of the hardware present on these nodes
 to properly build and test DPDK.
 """
-
-# pylama:ignore=W0611
-
-from .cpu import LogicalCoreCount, LogicalCoreCountFilter, LogicalCoreList
-from .node import Node
-from .port import Port, PortLink
-from .sut_node import SutNode
-from .tg_node import TGNode
-from .virtual_device import VirtualDevice
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
index 1a77aee532..e5f5fcbe0e 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -32,13 +32,13 @@
 from framework.logger import DTSLogger
 from framework.params import Params
 from framework.remote_session import (
-    CommandResult,
     InteractiveRemoteSession,
-    InteractiveShell,
     RemoteSession,
     create_interactive_session,
     create_remote_session,
 )
+from framework.remote_session.interactive_shell import InteractiveShell
+from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
 
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index e1163106a3..83ad06ae2d 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -26,7 +26,7 @@
 )
 from framework.params import Params, Switch
 from framework.params.eal import EalParams
-from framework.remote_session import CommandResult
+from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
 
diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py
index ed5467d825..7bc1c2cc08 100644
--- a/dts/framework/testbed_model/traffic_generator/scapy.py
+++ b/dts/framework/testbed_model/traffic_generator/scapy.py
@@ -25,7 +25,7 @@
 from scapy.packet import Packet  # type: ignore[import-untyped]
 
 from framework.config import OS, ScapyTrafficGeneratorConfig
-from framework.remote_session import PythonShell
+from framework.remote_session.python_shell import PythonShell
 from framework.settings import SETTINGS
 from framework.testbed_model.node import Node
 from framework.testbed_model.port import Port
diff --git a/dts/tests/TestSuite_hello_world.py b/dts/tests/TestSuite_hello_world.py
index fd7ff1534d..0d6995f260 100644
--- a/dts/tests/TestSuite_hello_world.py
+++ b/dts/tests/TestSuite_hello_world.py
@@ -8,7 +8,7 @@
 """
 
 from framework.test_suite import TestSuite
-from framework.testbed_model import (
+from framework.testbed_model.cpu import (
     LogicalCoreCount,
     LogicalCoreCountFilter,
     LogicalCoreList,
diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py
index a553e89662..ca678f662d 100644
--- a/dts/tests/TestSuite_smoke_tests.py
+++ b/dts/tests/TestSuite_smoke_tests.py
@@ -15,7 +15,7 @@
 import re
 
 from framework.config import PortConfig
-from framework.remote_session import TestPmdShell
+from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.settings import SETTINGS
 from framework.test_suite import TestSuite
 from framework.utils import REGEX_FOR_PCI_ADDRESS
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v5 5/8] dts: add testpmd shell params
  2024-06-17 14:54 ` [PATCH v5 0/8] dts: add testpmd params Luca Vizzarro
                     ` (3 preceding siblings ...)
  2024-06-17 14:54   ` [PATCH v5 4/8] dts: remove module-wide imports Luca Vizzarro
@ 2024-06-17 14:54   ` Luca Vizzarro
  2024-06-17 15:24     ` Nicholas Pratte
  2024-06-17 14:54   ` [PATCH v5 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
                     ` (2 subsequent siblings)
  7 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-17 14:54 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Implement all the testpmd shell parameters into a data structure.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 dts/framework/params/testpmd.py               | 607 ++++++++++++++++++
 dts/framework/remote_session/testpmd_shell.py |  39 +-
 dts/tests/TestSuite_pmd_buffer_scatter.py     |   5 +-
 3 files changed, 613 insertions(+), 38 deletions(-)
 create mode 100644 dts/framework/params/testpmd.py

diff --git a/dts/framework/params/testpmd.py b/dts/framework/params/testpmd.py
new file mode 100644
index 0000000000..1913bd0fa2
--- /dev/null
+++ b/dts/framework/params/testpmd.py
@@ -0,0 +1,607 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Module containing all the TestPmd-related parameter classes."""
+
+from dataclasses import dataclass, field
+from enum import EnumMeta, Flag, auto, unique
+from pathlib import PurePath
+from typing import Literal, NamedTuple
+
+from framework.params import (
+    Params,
+    Switch,
+    YesNoSwitch,
+    bracketed,
+    comma_separated,
+    hex_from_flag_value,
+    modify_str,
+    str_from_flag_value,
+)
+from framework.params.eal import EalParams
+from framework.utils import StrEnum
+
+
+class PortTopology(StrEnum):
+    """Enum representing the port topology."""
+
+    #: In paired mode, the forwarding is between pairs of ports, e.g.: (0,1), (2,3), (4,5).
+    paired = auto()
+
+    #: In chained mode, the forwarding is to the next available port in the port mask, e.g.:
+    #: (0,1), (1,2), (2,0).
+    #:
+    #: The ordering of the ports can be changed using the portlist testpmd runtime function.
+    chained = auto()
+
+    #: In loop mode, ingress traffic is simply transmitted back on the same interface.
+    loop = auto()
+
+
+@modify_str(comma_separated, bracketed)
+class PortNUMAConfig(NamedTuple):
+    """DPDK port to NUMA socket association tuple."""
+
+    #:
+    port: int
+    #:
+    socket: int
+
+
+@modify_str(str_from_flag_value)
+@unique
+class FlowDirection(Flag):
+    """Flag indicating the direction of the flow.
+
+    A bi-directional flow can be specified with the pipe:
+
+    >>> TestPmdFlowDirection.RX | TestPmdFlowDirection.TX
+    <TestPmdFlowDirection.TX|RX: 3>
+    """
+
+    #:
+    RX = 1 << 0
+    #:
+    TX = 1 << 1
+
+
+@modify_str(comma_separated, bracketed)
+class RingNUMAConfig(NamedTuple):
+    """Tuple associating DPDK port, direction of the flow and NUMA socket."""
+
+    #:
+    port: int
+    #:
+    direction: FlowDirection
+    #:
+    socket: int
+
+
+@modify_str(comma_separated)
+class EthPeer(NamedTuple):
+    """Tuple associating a MAC address to the specified DPDK port."""
+
+    #:
+    port_no: int
+    #:
+    mac_address: str
+
+
+@modify_str(comma_separated)
+class TxIPAddrPair(NamedTuple):
+    """Tuple specifying the source and destination IPs for the packets."""
+
+    #:
+    source_ip: str
+    #:
+    dest_ip: str
+
+
+@modify_str(comma_separated)
+class TxUDPPortPair(NamedTuple):
+    """Tuple specifying the UDP source and destination ports for the packets.
+
+    If leaving ``dest_port`` unspecified, ``source_port`` will be used for
+    the destination port as well.
+    """
+
+    #:
+    source_port: int
+    #:
+    dest_port: int | None = None
+
+
+@dataclass
+class DisableRSS(Params):
+    """Disables RSS (Receive Side Scaling)."""
+
+    _disable_rss: Literal[True] = field(
+        default=True, init=False, metadata=Params.long("disable-rss")
+    )
+
+
+@dataclass
+class SetRSSIPOnly(Params):
+    """Sets RSS (Receive Side Scaling) functions for IPv4/IPv6 only."""
+
+    _rss_ip: Literal[True] = field(default=True, init=False, metadata=Params.long("rss-ip"))
+
+
+@dataclass
+class SetRSSUDP(Params):
+    """Sets RSS (Receive Side Scaling) functions for IPv4/IPv6 and UDP."""
+
+    _rss_udp: Literal[True] = field(default=True, init=False, metadata=Params.long("rss-udp"))
+
+
+class RSSSetting(EnumMeta):
+    """Enum representing a RSS setting. Each property is a class that needs to be initialised."""
+
+    #:
+    Disabled = DisableRSS
+    #:
+    SetIPOnly = SetRSSIPOnly
+    #:
+    SetUDP = SetRSSUDP
+
+
+class SimpleForwardingModes(StrEnum):
+    r"""The supported packet forwarding modes for :class:`~TestPmdShell`\s."""
+
+    #:
+    io = auto()
+    #:
+    mac = auto()
+    #:
+    macswap = auto()
+    #:
+    rxonly = auto()
+    #:
+    csum = auto()
+    #:
+    icmpecho = auto()
+    #:
+    ieee1588 = auto()
+    #:
+    fivetswap = "5tswap"
+    #:
+    shared_rxq = "shared-rxq"
+    #:
+    recycle_mbufs = auto()
+
+
+@dataclass(kw_only=True)
+class TXOnlyForwardingMode(Params):
+    """Sets a TX-Only forwarding mode.
+
+    Attributes:
+        multi_flow: Generates multiple flows if set to True.
+        segments_length: Sets TX segment sizes or total packet length.
+    """
+
+    _forward_mode: Literal["txonly"] = field(
+        default="txonly", init=False, metadata=Params.long("forward-mode")
+    )
+    multi_flow: Switch = field(default=None, metadata=Params.long("txonly-multi-flow"))
+    segments_length: list[int] | None = field(
+        default=None, metadata=Params.long("txpkts") | Params.convert_value(comma_separated)
+    )
+
+
+@dataclass(kw_only=True)
+class FlowGenForwardingMode(Params):
+    """Sets a flowgen forwarding mode.
+
+    Attributes:
+        clones: Set the number of each packet clones to be sent. Sending clones reduces host CPU
+                load on creating packets and may help in testing extreme speeds or maxing out
+                Tx packet performance. N should be not zero, but less than ‘burst’ parameter.
+        flows: Set the number of flows to be generated, where 1 <= N <= INT32_MAX.
+        segments_length: Set TX segment sizes or total packet length.
+    """
+
+    _forward_mode: Literal["flowgen"] = field(
+        default="flowgen", init=False, metadata=Params.long("forward-mode")
+    )
+    clones: int | None = field(default=None, metadata=Params.long("flowgen-clones"))
+    flows: int | None = field(default=None, metadata=Params.long("flowgen-flows"))
+    segments_length: list[int] | None = field(
+        default=None, metadata=Params.long("txpkts") | Params.convert_value(comma_separated)
+    )
+
+
+@dataclass(kw_only=True)
+class NoisyForwardingMode(Params):
+    """Sets a noisy forwarding mode.
+
+    Attributes:
+        forward_mode: Set the noisy VNF forwarding mode.
+        tx_sw_buffer_size: Set the maximum number of elements of the FIFO queue to be created for
+                           buffering packets.
+        tx_sw_buffer_flushtime: Set the time before packets in the FIFO queue are flushed.
+        lkup_memory: Set the size of the noisy neighbor simulation memory buffer in MB to N.
+        lkup_num_reads: Set the size of the noisy neighbor simulation memory buffer in MB to N.
+        lkup_num_writes: Set the number of writes to be done in noisy neighbor simulation
+                         memory buffer to N.
+        lkup_num_reads_writes: Set the number of r/w accesses to be done in noisy neighbor
+                               simulation memory buffer to N.
+    """
+
+    _forward_mode: Literal["noisy"] = field(
+        default="noisy", init=False, metadata=Params.long("forward-mode")
+    )
+    forward_mode: (
+        Literal[
+            SimpleForwardingModes.io,
+            SimpleForwardingModes.mac,
+            SimpleForwardingModes.macswap,
+            SimpleForwardingModes.fivetswap,
+        ]
+        | None
+    ) = field(default=SimpleForwardingModes.io, metadata=Params.long("noisy-forward-mode"))
+    tx_sw_buffer_size: int | None = field(
+        default=None, metadata=Params.long("noisy-tx-sw-buffer-size")
+    )
+    tx_sw_buffer_flushtime: int | None = field(
+        default=None, metadata=Params.long("noisy-tx-sw-buffer-flushtime")
+    )
+    lkup_memory: int | None = field(default=None, metadata=Params.long("noisy-lkup-memory"))
+    lkup_num_reads: int | None = field(default=None, metadata=Params.long("noisy-lkup-num-reads"))
+    lkup_num_writes: int | None = field(default=None, metadata=Params.long("noisy-lkup-num-writes"))
+    lkup_num_reads_writes: int | None = field(
+        default=None, metadata=Params.long("noisy-lkup-num-reads-writes")
+    )
+
+
+@modify_str(hex_from_flag_value)
+@unique
+class HairpinMode(Flag):
+    """Flag representing the hairpin mode."""
+
+    #: Two hairpin ports loop.
+    TWO_PORTS_LOOP = 1 << 0
+    #: Two hairpin ports paired.
+    TWO_PORTS_PAIRED = 1 << 1
+    #: Explicit Tx flow rule.
+    EXPLICIT_TX_FLOW = 1 << 4
+    #: Force memory settings of hairpin RX queue.
+    FORCE_RX_QUEUE_MEM_SETTINGS = 1 << 8
+    #: Force memory settings of hairpin TX queue.
+    FORCE_TX_QUEUE_MEM_SETTINGS = 1 << 9
+    #: Hairpin RX queues will use locked device memory.
+    RX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 12
+    #: Hairpin RX queues will use RTE memory.
+    RX_QUEUE_USE_RTE_MEMORY = 1 << 13
+    #: Hairpin TX queues will use locked device memory.
+    TX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 16
+    #: Hairpin TX queues will use RTE memory.
+    TX_QUEUE_USE_RTE_MEMORY = 1 << 18
+
+
+@dataclass(kw_only=True)
+class RXRingParams(Params):
+    """Sets the RX ring parameters.
+
+    Attributes:
+        descriptors: Set the number of descriptors in the RX rings to N, where N > 0.
+        prefetch_threshold: Set the prefetch threshold register of RX rings to N, where N >= 0.
+        host_threshold: Set the host threshold register of RX rings to N, where N >= 0.
+        write_back_threshold: Set the write-back threshold register of RX rings to N, where N >= 0.
+        free_threshold: Set the free threshold of RX descriptors to N,
+                        where 0 <= N < value of ``-–rxd``.
+    """
+
+    descriptors: int | None = field(default=None, metadata=Params.long("rxd"))
+    prefetch_threshold: int | None = field(default=None, metadata=Params.long("rxpt"))
+    host_threshold: int | None = field(default=None, metadata=Params.long("rxht"))
+    write_back_threshold: int | None = field(default=None, metadata=Params.long("rxwt"))
+    free_threshold: int | None = field(default=None, metadata=Params.long("rxfreet"))
+
+
+@modify_str(hex_from_flag_value)
+@unique
+class RXMultiQueueMode(Flag):
+    """Flag representing the RX multi-queue mode."""
+
+    #:
+    RSS = 1 << 0
+    #:
+    DCB = 1 << 1
+    #:
+    VMDQ = 1 << 2
+
+
+@dataclass(kw_only=True)
+class TXRingParams(Params):
+    """Sets the TX ring parameters.
+
+    Attributes:
+        descriptors: Set the number of descriptors in the TX rings to N, where N > 0.
+        rs_bit_threshold: Set the transmit RS bit threshold of TX rings to N,
+                          where 0 <= N <= value of ``--txd``.
+        prefetch_threshold: Set the prefetch threshold register of TX rings to N, where N >= 0.
+        host_threshold: Set the host threshold register of TX rings to N, where N >= 0.
+        write_back_threshold: Set the write-back threshold register of TX rings to N, where N >= 0.
+        free_threshold: Set the transmit free threshold of TX rings to N,
+                        where 0 <= N <= value of ``--txd``.
+    """
+
+    descriptors: int | None = field(default=None, metadata=Params.long("txd"))
+    rs_bit_threshold: int | None = field(default=None, metadata=Params.long("txrst"))
+    prefetch_threshold: int | None = field(default=None, metadata=Params.long("txpt"))
+    host_threshold: int | None = field(default=None, metadata=Params.long("txht"))
+    write_back_threshold: int | None = field(default=None, metadata=Params.long("txwt"))
+    free_threshold: int | None = field(default=None, metadata=Params.long("txfreet"))
+
+
+class Event(StrEnum):
+    """Enum representing a testpmd event."""
+
+    #:
+    unknown = auto()
+    #:
+    queue_state = auto()
+    #:
+    vf_mbox = auto()
+    #:
+    macsec = auto()
+    #:
+    intr_lsc = auto()
+    #:
+    intr_rmv = auto()
+    #:
+    intr_reset = auto()
+    #:
+    dev_probed = auto()
+    #:
+    dev_released = auto()
+    #:
+    flow_aged = auto()
+    #:
+    err_recovering = auto()
+    #:
+    recovery_success = auto()
+    #:
+    recovery_failed = auto()
+    #:
+    all = auto()
+
+
+class SimpleMempoolAllocationMode(StrEnum):
+    """Enum representing simple mempool allocation modes."""
+
+    #: Create and populate mempool using native DPDK memory.
+    native = auto()
+    #: Create and populate mempool using externally and anonymously allocated area.
+    xmem = auto()
+    #: Create and populate mempool using externally and anonymously allocated hugepage area.
+    xmemhuge = auto()
+
+
+@dataclass(kw_only=True)
+class AnonMempoolAllocationMode(Params):
+    """Create mempool using native DPDK memory, but populate using anonymous memory.
+
+    Attributes:
+        no_iova_contig: Enables to create mempool which is not IOVA contiguous.
+    """
+
+    _mp_alloc: Literal["anon"] = field(default="anon", init=False, metadata=Params.long("mp-alloc"))
+    no_iova_contig: Switch = None
+
+
+@dataclass(slots=True, kw_only=True)
+class TestPmdParams(EalParams):
+    """The testpmd shell parameters.
+
+    Attributes:
+        interactive_mode: Run testpmd in interactive mode.
+        auto_start: Start forwarding on initialization.
+        tx_first: Start forwarding, after sending a burst of packets first.
+        stats_period: Display statistics every ``PERIOD`` seconds, if interactive mode is disabled.
+                      The default value is 0, which means that the statistics will not be displayed.
+
+                      .. note:: This flag should be used only in non-interactive mode.
+        display_xstats: Display comma-separated list of extended statistics every ``PERIOD`` seconds
+                        as specified in ``--stats-period`` or when used with interactive commands
+                        that show Rx/Tx statistics (i.e. ‘show port stats’).
+        nb_cores: Set the number of forwarding cores, where 1 <= N <= “number of cores” or
+                  ``RTE_MAX_LCORE`` from the configuration file.
+        coremask: Set the bitmask of the cores running the packet forwarding test. The main
+                  lcore is reserved for command line parsing only and cannot be masked on for packet
+                  forwarding.
+        nb_ports: Set the number of forwarding ports, where 1 <= N <= “number of ports” on the board
+                  or ``RTE_MAX_ETHPORTS`` from the configuration file. The default value is the
+                  number of ports on the board.
+        port_topology: Set port topology, where mode is paired (the default), chained or loop.
+        portmask: Set the bitmask of the ports used by the packet forwarding test.
+        portlist: Set the forwarding ports based on the user input used by the packet forwarding
+                  test. ‘-‘ denotes a range of ports to set including the two specified port IDs ‘,’
+                  separates multiple port values. Possible examples like –portlist=0,1 or
+                  –portlist=0-2 or –portlist=0,1-2 etc.
+        numa: Enable/disable NUMA-aware allocation of RX/TX rings and of RX memory buffers (mbufs).
+        socket_num: Set the socket from which all memory is allocated in NUMA mode, where
+                    0 <= N < number of sockets on the board.
+        port_numa_config: Specify the socket on which the memory pool to be used by the port will be
+                          allocated.
+        ring_numa_config: Specify the socket on which the TX/RX rings for the port will be
+                          allocated. Where flag is 1 for RX, 2 for TX, and 3 for RX and TX.
+        total_num_mbufs: Set the number of mbufs to be allocated in the mbuf pools, where N > 1024.
+        mbuf_size: Set the data size of the mbufs used to N bytes, where N < 65536.
+                   If multiple mbuf-size values are specified the extra memory pools will be created
+                   for allocating mbufs to receive packets with buffer splitting features.
+        mbcache: Set the cache of mbuf memory pools to N, where 0 <= N <= 512.
+        max_pkt_len: Set the maximum packet size to N bytes, where N >= 64.
+        eth_peers_configfile: Use a configuration file containing the Ethernet addresses of
+                              the peer ports.
+        eth_peer: Set the MAC address XX:XX:XX:XX:XX:XX of the peer port N,
+                  where 0 <= N < RTE_MAX_ETHPORTS.
+        tx_ip: Set the source and destination IP address used when doing transmit only test.
+               The defaults address values are source 198.18.0.1 and destination 198.18.0.2.
+               These are special purpose addresses reserved for benchmarking (RFC 5735).
+        tx_udp: Set the source and destination UDP port number for transmit test only test.
+                The default port is the port 9 which is defined for the discard protocol (RFC 863).
+        enable_lro: Enable large receive offload.
+        max_lro_pkt_size: Set the maximum LRO aggregated packet size to N bytes, where N >= 64.
+        disable_crc_strip: Disable hardware CRC stripping.
+        enable_scatter: Enable scatter (multi-segment) RX.
+        enable_hw_vlan: Enable hardware VLAN.
+        enable_hw_vlan_filter: Enable hardware VLAN filter.
+        enable_hw_vlan_strip: Enable hardware VLAN strip.
+        enable_hw_vlan_extend: Enable hardware VLAN extend.
+        enable_hw_qinq_strip: Enable hardware QINQ strip.
+        pkt_drop_enabled: Enable per-queue packet drop for packets with no descriptors.
+        rss: Receive Side Scaling setting.
+        forward_mode: Set the forwarding mode.
+        hairpin_mode: Set the hairpin port configuration.
+        hairpin_queues: Set the number of hairpin queues per port to N, where 1 <= N <= 65535.
+        burst: Set the number of packets per burst to N, where 1 <= N <= 512.
+        enable_rx_cksum: Enable hardware RX checksum offload.
+        rx_queues: Set the number of RX queues per port to N, where 1 <= N <= 65535.
+        rx_ring: Set the RX rings parameters.
+        no_flush_rx: Don’t flush the RX streams before starting forwarding. Used mainly with
+                     the PCAP PMD.
+        rx_segments_offsets: Set the offsets of packet segments on receiving
+                             if split feature is engaged.
+        rx_segments_length: Set the length of segments to scatter packets on receiving
+                            if split feature is engaged.
+        multi_rx_mempool: Enable multiple mbuf pools per Rx queue.
+        rx_shared_queue: Create queues in shared Rx queue mode if device supports. Shared Rx queues
+                         are grouped per X ports. X defaults to UINT32_MAX, implies all ports join
+                         share group 1. Forwarding engine “shared-rxq” should be used for shared Rx
+                         queues. This engine does Rx only and update stream statistics accordingly.
+        rx_offloads: Set the bitmask of RX queue offloads.
+        rx_mq_mode: Set the RX multi queue mode which can be enabled.
+        tx_queues: Set the number of TX queues per port to N, where 1 <= N <= 65535.
+        tx_ring: Set the TX rings params.
+        tx_offloads: Set the hexadecimal bitmask of TX queue offloads.
+        eth_link_speed: Set a forced link speed to the ethernet port. E.g. 1000 for 1Gbps.
+        disable_link_check: Disable check on link status when starting/stopping ports.
+        disable_device_start: Do not automatically start all ports. This allows testing
+                              configuration of rx and tx queues before device is started
+                              for the first time.
+        no_lsc_interrupt: Disable LSC interrupts for all ports, even those supporting it.
+        no_rmv_interrupt: Disable RMV interrupts for all ports, even those supporting it.
+        bitrate_stats: Set the logical core N to perform bitrate calculation.
+        latencystats: Set the logical core N to perform latency and jitter calculations.
+        print_events: Enable printing the occurrence of the designated events.
+                      Using :attr:`TestPmdEvent.ALL` will enable all of them.
+        mask_events: Disable printing the occurrence of the designated events.
+                     Using :attr:`TestPmdEvent.ALL` will disable all of them.
+        flow_isolate_all: Providing this parameter requests flow API isolated mode on all ports at
+                          initialization time. It ensures all traffic is received through the
+                          configured flow rules only (see flow command). Ports that do not support
+                          this mode are automatically discarded.
+        disable_flow_flush: Disable port flow flush when stopping port.
+                            This allows testing keep flow rules or shared flow objects across
+                            restart.
+        hot_plug: Enable device event monitor mechanism for hotplug.
+        vxlan_gpe_port: Set the UDP port number of tunnel VXLAN-GPE to N.
+        geneve_parsed_port: Set the UDP port number that is used for parsing the GENEVE protocol
+                            to N. HW may be configured with another tunnel Geneve port.
+        lock_all_memory: Enable/disable locking all memory. Disabled by default.
+        mempool_allocation_mode: Set mempool allocation mode.
+        record_core_cycles: Enable measurement of CPU cycles per packet.
+        record_burst_status: Enable display of RX and TX burst stats.
+    """
+
+    interactive_mode: Switch = field(default=True, metadata=Params.short("i"))
+    auto_start: Switch = field(default=None, metadata=Params.short("a"))
+    tx_first: Switch = None
+    stats_period: int | None = None
+    display_xstats: list[str] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    nb_cores: int | None = None
+    coremask: int | None = field(default=None, metadata=Params.convert_value(hex))
+    nb_ports: int | None = None
+    port_topology: PortTopology | None = PortTopology.paired
+    portmask: int | None = field(default=None, metadata=Params.convert_value(hex))
+    portlist: str | None = None  # TODO: can be ranges 0,1-3
+
+    numa: YesNoSwitch = None
+    socket_num: int | None = None
+    port_numa_config: list[PortNUMAConfig] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    ring_numa_config: list[RingNUMAConfig] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    total_num_mbufs: int | None = None
+    mbuf_size: list[int] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    mbcache: int | None = None
+    max_pkt_len: int | None = None
+    eth_peers_configfile: PurePath | None = None
+    eth_peer: list[EthPeer] | None = field(default=None, metadata=Params.multiple())
+    tx_ip: TxIPAddrPair | None = None
+    tx_udp: TxUDPPortPair | None = None
+    enable_lro: Switch = None
+    max_lro_pkt_size: int | None = None
+    disable_crc_strip: Switch = None
+    enable_scatter: Switch = None
+    enable_hw_vlan: Switch = None
+    enable_hw_vlan_filter: Switch = None
+    enable_hw_vlan_strip: Switch = None
+    enable_hw_vlan_extend: Switch = None
+    enable_hw_qinq_strip: Switch = None
+    pkt_drop_enabled: Switch = field(default=None, metadata=Params.long("enable-drop-en"))
+    rss: RSSSetting | None = None
+    forward_mode: (
+        SimpleForwardingModes
+        | FlowGenForwardingMode
+        | TXOnlyForwardingMode
+        | NoisyForwardingMode
+        | None
+    ) = None
+    hairpin_mode: HairpinMode | None = None
+    hairpin_queues: int | None = field(default=None, metadata=Params.long("hairpinq"))
+    burst: int | None = None
+    enable_rx_cksum: Switch = None
+
+    rx_queues: int | None = field(default=None, metadata=Params.long("rxq"))
+    rx_ring: RXRingParams | None = None
+    no_flush_rx: Switch = None
+    rx_segments_offsets: list[int] | None = field(
+        default=None, metadata=Params.long("rxoffs") | Params.convert_value(comma_separated)
+    )
+    rx_segments_length: list[int] | None = field(
+        default=None, metadata=Params.long("rxpkts") | Params.convert_value(comma_separated)
+    )
+    multi_rx_mempool: Switch = None
+    rx_shared_queue: Switch | int = field(default=None, metadata=Params.long("rxq-share"))
+    rx_offloads: int | None = field(default=None, metadata=Params.convert_value(hex))
+    rx_mq_mode: RXMultiQueueMode | None = None
+
+    tx_queues: int | None = field(default=None, metadata=Params.long("txq"))
+    tx_ring: TXRingParams | None = None
+    tx_offloads: int | None = field(default=None, metadata=Params.convert_value(hex))
+
+    eth_link_speed: int | None = None
+    disable_link_check: Switch = None
+    disable_device_start: Switch = None
+    no_lsc_interrupt: Switch = None
+    no_rmv_interrupt: Switch = None
+    bitrate_stats: int | None = None
+    latencystats: int | None = None
+    print_events: list[Event] | None = field(
+        default=None, metadata=Params.multiple() | Params.long("print-event")
+    )
+    mask_events: list[Event] | None = field(
+        default_factory=lambda: [Event.intr_lsc],
+        metadata=Params.multiple() | Params.long("mask-event"),
+    )
+
+    flow_isolate_all: Switch = None
+    disable_flow_flush: Switch = None
+
+    hot_plug: Switch = None
+    vxlan_gpe_port: int | None = None
+    geneve_parsed_port: int | None = None
+    lock_all_memory: YesNoSwitch = field(default=None, metadata=Params.long("mlockall"))
+    mempool_allocation_mode: SimpleMempoolAllocationMode | AnonMempoolAllocationMode | None = field(
+        default=None, metadata=Params.long("mp-alloc")
+    )
+    record_core_cycles: Switch = None
+    record_burst_status: Switch = None
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index 2b9ef9418d..82701a9839 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -26,7 +26,7 @@
 from typing_extensions import Self
 
 from framework.exception import InteractiveCommandExecutionError
-from framework.params.eal import EalParams
+from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
 from framework.parser import ParserFn, TextParser
 from framework.settings import SETTINGS
 from framework.utils import StrEnum
@@ -56,37 +56,6 @@ def __str__(self) -> str:
         return self.pci_address
 
 
-class TestPmdForwardingModes(StrEnum):
-    r"""The supported packet forwarding modes for :class:`~TestPmdShell`\s."""
-
-    #:
-    io = auto()
-    #:
-    mac = auto()
-    #:
-    macswap = auto()
-    #:
-    flowgen = auto()
-    #:
-    rxonly = auto()
-    #:
-    txonly = auto()
-    #:
-    csum = auto()
-    #:
-    icmpecho = auto()
-    #:
-    ieee1588 = auto()
-    #:
-    noisy = auto()
-    #:
-    fivetswap = "5tswap"
-    #:
-    shared_rxq = "shared-rxq"
-    #:
-    recycle_mbufs = auto()
-
-
 class VLANOffloadFlag(Flag):
     """Flag representing the VLAN offload settings of a NIC port."""
 
@@ -646,9 +615,7 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
         Also find the number of pci addresses which were allowed on the command line when the app
         was started.
         """
-        self._app_params += " -i --mask-event intr_lsc"
-
-        assert isinstance(self._app_params, EalParams)
+        assert isinstance(self._app_params, TestPmdParams)
 
         self.number_of_ports = (
             len(self._app_params.ports) if self._app_params.ports is not None else 0
@@ -740,7 +707,7 @@ def wait_link_status_up(self, port_id: int, timeout=SETTINGS.timeout) -> bool:
             self._logger.error(f"The link for port {port_id} did not come up in the given timeout.")
         return "Link status: up" in port_info
 
-    def set_forward_mode(self, mode: TestPmdForwardingModes, verify: bool = True):
+    def set_forward_mode(self, mode: SimpleForwardingModes, verify: bool = True):
         """Set packet forwarding mode.
 
         Args:
diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index c6e93839cb..578b5a4318 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -23,7 +23,8 @@
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
 from framework.params import Params
-from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
+from framework.params.testpmd import SimpleForwardingModes
+from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.test_suite import TestSuite
 
 
@@ -113,7 +114,7 @@ def pmd_scatter(self, mbsize: int) -> None:
             ),
             privileged=True,
         )
-        testpmd.set_forward_mode(TestPmdForwardingModes.mac)
+        testpmd.set_forward_mode(SimpleForwardingModes.mac)
         testpmd.start()
 
         for offset in [-1, 0, 1, 4, 5]:
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v5 6/8] dts: use testpmd params for scatter test suite
  2024-06-17 14:54 ` [PATCH v5 0/8] dts: add testpmd params Luca Vizzarro
                     ` (4 preceding siblings ...)
  2024-06-17 14:54   ` [PATCH v5 5/8] dts: add testpmd shell params Luca Vizzarro
@ 2024-06-17 14:54   ` Luca Vizzarro
  2024-06-17 15:24     ` Nicholas Pratte
  2024-06-17 14:54   ` [PATCH v5 7/8] dts: rework interactive shells Luca Vizzarro
  2024-06-17 14:54   ` [PATCH v5 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
  7 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-17 14:54 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Update the buffer scatter test suite to use TestPmdParameters
instead of the StrParams implementation.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 dts/tests/TestSuite_pmd_buffer_scatter.py | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index 578b5a4318..6d206c1a40 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -16,14 +16,14 @@
 """
 
 import struct
+from dataclasses import asdict
 
 from scapy.layers.inet import IP  # type: ignore[import-untyped]
 from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
 from scapy.packet import Raw  # type: ignore[import-untyped]
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
-from framework.params import Params
-from framework.params.testpmd import SimpleForwardingModes
+from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
 from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.test_suite import TestSuite
 
@@ -105,16 +105,16 @@ def pmd_scatter(self, mbsize: int) -> None:
         """
         testpmd = self.sut_node.create_interactive_shell(
             TestPmdShell,
-            app_params=Params.from_str(
-                "--mbcache=200 "
-                f"--mbuf-size={mbsize} "
-                "--max-pkt-len=9000 "
-                "--port-topology=paired "
-                "--tx-offloads=0x00008000"
+            app_params=TestPmdParams(
+                forward_mode=SimpleForwardingModes.mac,
+                mbcache=200,
+                mbuf_size=[mbsize],
+                max_pkt_len=9000,
+                tx_offloads=0x00008000,
+                **asdict(self.sut_node.create_eal_parameters()),
             ),
             privileged=True,
         )
-        testpmd.set_forward_mode(SimpleForwardingModes.mac)
         testpmd.start()
 
         for offset in [-1, 0, 1, 4, 5]:
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v5 7/8] dts: rework interactive shells
  2024-06-17 14:54 ` [PATCH v5 0/8] dts: add testpmd params Luca Vizzarro
                     ` (5 preceding siblings ...)
  2024-06-17 14:54   ` [PATCH v5 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
@ 2024-06-17 14:54   ` Luca Vizzarro
  2024-06-17 15:25     ` Nicholas Pratte
  2024-06-17 14:54   ` [PATCH v5 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
  7 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-17 14:54 UTC (permalink / raw)
  To: dev; +Cc: Jeremy Spewock, Juraj Linkeš, Luca Vizzarro, Paul Szczepanek

The way nodes and interactive shells interact makes it difficult to
develop for static type checking and hinting. The current system relies
on a top-down approach, attempting to give a generic interface to the
test developer, hiding the interaction of concrete shell classes as much
as possible. When working with strong typing this approach is not ideal,
as Python's implementation of generics is still rudimentary.

This rework reverses the tests interaction to a bottom-up approach,
allowing the test developer to call concrete shell classes directly,
and let them ingest nodes independently. While also re-enforcing type
checking and making the code easier to read.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 dts/framework/params/eal.py                   |   6 +-
 dts/framework/remote_session/dpdk_shell.py    | 106 +++++++++++++++
 .../remote_session/interactive_shell.py       |  79 ++++++-----
 dts/framework/remote_session/python_shell.py  |   4 +-
 dts/framework/remote_session/testpmd_shell.py |  64 +++++----
 dts/framework/testbed_model/node.py           |  36 +----
 dts/framework/testbed_model/os_session.py     |  36 +----
 dts/framework/testbed_model/sut_node.py       | 124 +-----------------
 .../testbed_model/traffic_generator/scapy.py  |   4 +-
 dts/tests/TestSuite_hello_world.py            |   7 +-
 dts/tests/TestSuite_pmd_buffer_scatter.py     |  21 ++-
 dts/tests/TestSuite_smoke_tests.py            |   2 +-
 12 files changed, 210 insertions(+), 279 deletions(-)
 create mode 100644 dts/framework/remote_session/dpdk_shell.py

diff --git a/dts/framework/params/eal.py b/dts/framework/params/eal.py
index bbdbc8f334..8d7766fefc 100644
--- a/dts/framework/params/eal.py
+++ b/dts/framework/params/eal.py
@@ -35,9 +35,9 @@ class EalParams(Params):
                 ``other_eal_param='--single-file-segments'``
     """
 
-    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
-    memory_channels: int = field(metadata=Params.short("n"))
-    prefix: str = field(metadata=Params.long("file-prefix"))
+    lcore_list: LogicalCoreList | None = field(default=None, metadata=Params.short("l"))
+    memory_channels: int | None = field(default=None, metadata=Params.short("n"))
+    prefix: str = field(default="dpdk", metadata=Params.long("file-prefix"))
     no_pci: Switch = None
     vdevs: list[VirtualDevice] | None = field(
         default=None, metadata=Params.multiple() | Params.long("vdev")
diff --git a/dts/framework/remote_session/dpdk_shell.py b/dts/framework/remote_session/dpdk_shell.py
new file mode 100644
index 0000000000..2cbf69ae9a
--- /dev/null
+++ b/dts/framework/remote_session/dpdk_shell.py
@@ -0,0 +1,106 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Base interactive shell for DPDK applications.
+
+Provides a base class to create interactive shells based on DPDK.
+"""
+
+
+from abc import ABC
+
+from framework.params.eal import EalParams
+from framework.remote_session.interactive_shell import InteractiveShell
+from framework.settings import SETTINGS
+from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
+from framework.testbed_model.sut_node import SutNode
+
+
+def compute_eal_params(
+    sut_node: SutNode,
+    params: EalParams | None = None,
+    lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
+    ascending_cores: bool = True,
+    append_prefix_timestamp: bool = True,
+) -> EalParams:
+    """Compute EAL parameters based on the node's specifications.
+
+    Args:
+        sut_node: The SUT node to compute the values for.
+        params: If set to None a new object is created and returned. Otherwise the given
+            :class:`EalParams`'s lcore_list is modified according to the given filter specifier.
+            A DPDK prefix is added. If ports is set to None, all the SUT node's ports are
+            automatically assigned.
+        lcore_filter_specifier: A number of lcores/cores/sockets to use or a list of lcore ids to
+            use. The default will select one lcore for each of two cores on one socket, in ascending
+            order of core ids.
+        ascending_cores: Sort cores in ascending order (lowest to highest IDs). If :data:`False`,
+            sort in descending order.
+        append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.
+    """
+    if params is None:
+        params = EalParams()
+
+    if params.lcore_list is None:
+        params.lcore_list = LogicalCoreList(
+            sut_node.filter_lcores(lcore_filter_specifier, ascending_cores)
+        )
+
+    prefix = params.prefix
+    if append_prefix_timestamp:
+        prefix = f"{prefix}_{sut_node.dpdk_timestamp}"
+    prefix = sut_node.main_session.get_dpdk_file_prefix(prefix)
+    if prefix:
+        sut_node.dpdk_prefix_list.append(prefix)
+    params.prefix = prefix
+
+    if params.ports is None:
+        params.ports = sut_node.ports
+
+    return params
+
+
+class DPDKShell(InteractiveShell, ABC):
+    """The base class for managing DPDK-based interactive shells.
+
+    This class shouldn't be instantiated directly, but instead be extended.
+    It automatically injects computed EAL parameters based on the node in the
+    supplied app parameters.
+    """
+
+    _node: SutNode
+    _app_params: EalParams
+
+    _lcore_filter_specifier: LogicalCoreCount | LogicalCoreList
+    _ascending_cores: bool
+    _append_prefix_timestamp: bool
+
+    def __init__(
+        self,
+        node: SutNode,
+        privileged: bool = True,
+        timeout: float = SETTINGS.timeout,
+        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
+        ascending_cores: bool = True,
+        append_prefix_timestamp: bool = True,
+        start_on_init: bool = True,
+        app_params: EalParams = EalParams(),
+    ) -> None:
+        """Overrides :meth:`~.interactive_shell.InteractiveShell.__init__`."""
+        self._lcore_filter_specifier = lcore_filter_specifier
+        self._ascending_cores = ascending_cores
+        self._append_prefix_timestamp = append_prefix_timestamp
+
+        super().__init__(node, privileged, timeout, start_on_init, app_params)
+
+    def _post_init(self):
+        """Computes EAL params based on the node capabilities before start."""
+        self._app_params = compute_eal_params(
+            self._node,
+            self._app_params,
+            self._lcore_filter_specifier,
+            self._ascending_cores,
+            self._append_prefix_timestamp,
+        )
+
+        self._update_path(self._node.remote_dpdk_build_dir.joinpath(self.path))
diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
index 8191b36630..5a8a6d6d15 100644
--- a/dts/framework/remote_session/interactive_shell.py
+++ b/dts/framework/remote_session/interactive_shell.py
@@ -17,13 +17,14 @@
 
 from abc import ABC
 from pathlib import PurePath
-from typing import Callable, ClassVar
+from typing import ClassVar
 
-from paramiko import Channel, SSHClient, channel  # type: ignore[import-untyped]
+from paramiko import Channel, channel  # type: ignore[import-untyped]
 
 from framework.logger import DTSLogger
 from framework.params import Params
 from framework.settings import SETTINGS
+from framework.testbed_model.node import Node
 
 
 class InteractiveShell(ABC):
@@ -36,13 +37,14 @@ class InteractiveShell(ABC):
     session.
     """
 
-    _interactive_session: SSHClient
+    _node: Node
     _stdin: channel.ChannelStdinFile
     _stdout: channel.ChannelFile
     _ssh_channel: Channel
     _logger: DTSLogger
     _timeout: float
     _app_params: Params
+    _privileged: bool
 
     #: Prompt to expect at the end of output when sending a command.
     #: This is often overridden by subclasses.
@@ -56,56 +58,63 @@ class InteractiveShell(ABC):
     #: Path to the executable to start the interactive application.
     path: ClassVar[PurePath]
 
-    #: Whether this application is a DPDK app. If it is, the build directory
-    #: for DPDK on the node will be prepended to the path to the executable.
-    dpdk_app: ClassVar[bool] = False
-
     def __init__(
         self,
-        interactive_session: SSHClient,
-        logger: DTSLogger,
-        get_privileged_command: Callable[[str], str] | None,
-        app_params: Params = Params(),
+        node: Node,
+        privileged: bool = False,
         timeout: float = SETTINGS.timeout,
+        start_on_init: bool = True,
+        app_params: Params = Params(),
     ) -> None:
         """Create an SSH channel during initialization.
 
         Args:
-            interactive_session: The SSH session dedicated to interactive shells.
-            logger: The logger instance this session will use.
-            get_privileged_command: A method for modifying a command to allow it to use
-                elevated privileges. If :data:`None`, the application will not be started
-                with elevated privileges.
-            app_params: The command line parameters to be passed to the application on startup.
+            node: The node on which to run start the interactive shell.
+            privileged: Enables the shell to run as superuser.
             timeout: The timeout used for the SSH channel that is dedicated to this interactive
                 shell. This timeout is for collecting output, so if reading from the buffer
                 and no output is gathered within the timeout, an exception is thrown.
+            start_on_init: Start interactive shell automatically after object initialisation.
+            app_params: The command line parameters to be passed to the application on startup.
         """
-        self._interactive_session = interactive_session
-        self._ssh_channel = self._interactive_session.invoke_shell()
+        self._node = node
+        self._logger = node._logger
+        self._app_params = app_params
+        self._privileged = privileged
+        self._timeout = timeout
+        # Ensure path is properly formatted for the host
+        self._update_path(self._node.main_session.join_remote_path(self.path))
+
+        self._post_init()
+
+        if start_on_init:
+            self.start_application()
+
+    def _post_init(self):
+        """Overridable. Method called after the object init and before application start."""
+
+    def _setup_ssh_channel(self):
+        self._ssh_channel = self._node.main_session.interactive_session.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.settimeout(self._timeout)
         self._ssh_channel.set_combine_stderr(True)  # combines stdout and stderr streams
-        self._logger = logger
-        self._timeout = timeout
-        self._app_params = app_params
-        self._start_application(get_privileged_command)
 
-    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
+    def _make_start_command(self) -> str:
+        """Makes the command that starts the interactive shell."""
+        start_command = f"{self.path} {self._app_params or ''}"
+        if self._privileged:
+            start_command = self._node.main_session._get_privileged_command(start_command)
+        return start_command
+
+    def start_application(self) -> None:
         """Starts a new interactive application based on the path to the app.
 
         This method is often overridden by subclasses as their process for
         starting may look different.
-
-        Args:
-            get_privileged_command: A function (but could be any callable) that produces
-                the version of the command with elevated privileges.
         """
-        start_command = f"{self.path} {self._app_params}"
-        if get_privileged_command is not None:
-            start_command = get_privileged_command(start_command)
-        self.send_command(start_command)
+        self._setup_ssh_channel()
+        self.send_command(self._make_start_command())
 
     def send_command(
         self, command: str, prompt: str | None = None, skip_first_line: bool = False
@@ -156,3 +165,7 @@ def close(self) -> None:
     def __del__(self) -> None:
         """Make sure the session is properly closed before deleting the object."""
         self.close()
+
+    @classmethod
+    def _update_path(cls, path: PurePath) -> None:
+        cls.path = path
diff --git a/dts/framework/remote_session/python_shell.py b/dts/framework/remote_session/python_shell.py
index ccfd3783e8..953ed100df 100644
--- a/dts/framework/remote_session/python_shell.py
+++ b/dts/framework/remote_session/python_shell.py
@@ -6,9 +6,7 @@
 Typical usage example in a TestSuite::
 
     from framework.remote_session import PythonShell
-    python_shell = self.tg_node.create_interactive_shell(
-        PythonShell, timeout=5, privileged=True
-    )
+    python_shell = PythonShell(self.tg_node, timeout=5, privileged=True)
     python_shell.send_command("print('Hello World')")
     python_shell.close()
 """
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index 82701a9839..8ee6829067 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -7,9 +7,7 @@
 
 Typical usage example in a TestSuite::
 
-    testpmd_shell = self.sut_node.create_interactive_shell(
-            TestPmdShell, privileged=True
-        )
+    testpmd_shell = TestPmdShell(self.sut_node)
     devices = testpmd_shell.get_devices()
     for device in devices:
         print(device)
@@ -21,18 +19,19 @@
 from dataclasses import dataclass, field
 from enum import Flag, auto
 from pathlib import PurePath
-from typing import Callable, ClassVar
+from typing import ClassVar
 
 from typing_extensions import Self
 
 from framework.exception import InteractiveCommandExecutionError
 from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
 from framework.parser import ParserFn, TextParser
+from framework.remote_session.dpdk_shell import DPDKShell
 from framework.settings import SETTINGS
+from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
+from framework.testbed_model.sut_node import SutNode
 from framework.utils import StrEnum
 
-from .interactive_shell import InteractiveShell
-
 
 class TestPmdDevice(object):
     """The data of a device that testpmd can recognize.
@@ -577,52 +576,48 @@ class TestPmdPortStats(TextParser):
     tx_bps: int = field(metadata=TextParser.find_int(r"Tx-bps:\s+(\d+)"))
 
 
-class TestPmdShell(InteractiveShell):
+class TestPmdShell(DPDKShell):
     """Testpmd interactive shell.
 
     The testpmd shell users should never use
     the :meth:`~.interactive_shell.InteractiveShell.send_command` method directly, but rather
     call specialized methods. If there isn't one that satisfies a need, it should be added.
-
-    Attributes:
-        number_of_ports: The number of ports which were allowed on the command-line when testpmd
-            was started.
     """
 
-    number_of_ports: int
+    _app_params: TestPmdParams
 
     #: The path to the testpmd executable.
     path: ClassVar[PurePath] = PurePath("app", "dpdk-testpmd")
 
-    #: Flag this as a DPDK app so that it's clear this is not a system app and
-    #: needs to be looked in a specific path.
-    dpdk_app: ClassVar[bool] = True
-
     #: The testpmd's prompt.
     _default_prompt: ClassVar[str] = "testpmd>"
 
     #: This forces the prompt to appear after sending a command.
     _command_extra_chars: ClassVar[str] = "\n"
 
-    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
-        """Overrides :meth:`~.interactive_shell._start_application`.
-
-        Add flags for starting testpmd in interactive mode and disabling messages for link state
-        change events before starting the application. Link state is verified before starting
-        packet forwarding and the messages create unexpected newlines in the terminal which
-        complicates output collection.
-
-        Also find the number of pci addresses which were allowed on the command line when the app
-        was started.
-        """
-        assert isinstance(self._app_params, TestPmdParams)
-
-        self.number_of_ports = (
-            len(self._app_params.ports) if self._app_params.ports is not None else 0
+    def __init__(
+        self,
+        node: SutNode,
+        privileged: bool = True,
+        timeout: float = SETTINGS.timeout,
+        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
+        ascending_cores: bool = True,
+        append_prefix_timestamp: bool = True,
+        start_on_init: bool = True,
+        **app_params,
+    ) -> None:
+        """Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs."""
+        super().__init__(
+            node,
+            privileged,
+            timeout,
+            lcore_filter_specifier,
+            ascending_cores,
+            append_prefix_timestamp,
+            start_on_init,
+            TestPmdParams(**app_params),
         )
 
-        super()._start_application(get_privileged_command)
-
     def start(self, verify: bool = True) -> None:
         """Start packet forwarding with the current configuration.
 
@@ -642,7 +637,8 @@ def start(self, verify: bool = True) -> None:
                 self._logger.debug(f"Failed to start packet forwarding: \n{start_cmd_output}")
                 raise InteractiveCommandExecutionError("Testpmd failed to start packet forwarding.")
 
-            for port_id in range(self.number_of_ports):
+            number_of_ports = len(self._app_params.ports or [])
+            for port_id in range(number_of_ports):
                 if not self.wait_link_status_up(port_id):
                     raise InteractiveCommandExecutionError(
                         "Not all ports came up after starting packet forwarding in testpmd."
diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
index 6af4f25a3c..88395faabe 100644
--- a/dts/framework/testbed_model/node.py
+++ b/dts/framework/testbed_model/node.py
@@ -15,7 +15,7 @@
 
 from abc import ABC
 from ipaddress import IPv4Interface, IPv6Interface
-from typing import Any, Callable, Type, Union
+from typing import Any, Callable, Union
 
 from framework.config import (
     OS,
@@ -25,7 +25,6 @@
 )
 from framework.exception import ConfigurationError
 from framework.logger import DTSLogger, get_dts_logger
-from framework.params import Params
 from framework.settings import SETTINGS
 
 from .cpu import (
@@ -36,7 +35,7 @@
     lcore_filter,
 )
 from .linux_session import LinuxSession
-from .os_session import InteractiveShellType, OSSession
+from .os_session import OSSession
 from .port import Port
 from .virtual_device import VirtualDevice
 
@@ -196,37 +195,6 @@ def create_session(self, name: str) -> OSSession:
         self._other_sessions.append(connection)
         return connection
 
-    def create_interactive_shell(
-        self,
-        shell_cls: Type[InteractiveShellType],
-        timeout: float = SETTINGS.timeout,
-        privileged: bool = False,
-        app_params: Params = Params(),
-    ) -> InteractiveShellType:
-        """Factory for interactive session handlers.
-
-        Instantiate `shell_cls` according to the remote OS specifics.
-
-        Args:
-            shell_cls: The class of the shell.
-            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.
-            privileged: Whether to run the shell with administrative privileges.
-            app_args: The arguments to be passed to the application.
-
-        Returns:
-            An instance of the desired interactive application shell.
-        """
-        if not shell_cls.dpdk_app:
-            shell_cls.path = self.main_session.join_remote_path(shell_cls.path)
-
-        return self.main_session.create_interactive_shell(
-            shell_cls,
-            timeout,
-            privileged,
-            app_params,
-        )
-
     def filter_lcores(
         self,
         filter_specifier: LogicalCoreCount | LogicalCoreList,
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
index e5f5fcbe0e..e7e6c9d670 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -26,18 +26,16 @@
 from collections.abc import Iterable
 from ipaddress import IPv4Interface, IPv6Interface
 from pathlib import PurePath
-from typing import Type, TypeVar, Union
+from typing import Union
 
 from framework.config import Architecture, NodeConfiguration, NodeInfo
 from framework.logger import DTSLogger
-from framework.params import Params
 from framework.remote_session import (
     InteractiveRemoteSession,
     RemoteSession,
     create_interactive_session,
     create_remote_session,
 )
-from framework.remote_session.interactive_shell import InteractiveShell
 from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
@@ -45,8 +43,6 @@
 from .cpu import LogicalCore
 from .port import Port
 
-InteractiveShellType = TypeVar("InteractiveShellType", bound=InteractiveShell)
-
 
 class OSSession(ABC):
     """OS-unaware to OS-aware translation API definition.
@@ -131,36 +127,6 @@ def send_command(
 
         return self.remote_session.send_command(command, timeout, verify, env)
 
-    def create_interactive_shell(
-        self,
-        shell_cls: Type[InteractiveShellType],
-        timeout: float,
-        privileged: bool,
-        app_args: Params,
-    ) -> InteractiveShellType:
-        """Factory for interactive session handlers.
-
-        Instantiate `shell_cls` according to the remote OS specifics.
-
-        Args:
-            shell_cls: The class of the shell.
-            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.
-            privileged: Whether to run the shell with administrative privileges.
-            app_args: The arguments to be passed to the application.
-
-        Returns:
-            An instance of the desired interactive application shell.
-        """
-        return shell_cls(
-            self.interactive_session.session,
-            self._logger,
-            self._get_privileged_command if privileged else None,
-            app_args,
-            timeout,
-        )
-
     @staticmethod
     @abstractmethod
     def _get_privileged_command(command: str) -> str:
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index 83ad06ae2d..d231a01425 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -16,7 +16,6 @@
 import tarfile
 import time
 from pathlib import PurePath
-from typing import Type
 
 from framework.config import (
     BuildTargetConfiguration,
@@ -24,17 +23,13 @@
     NodeInfo,
     SutNodeConfiguration,
 )
-from framework.params import Params, Switch
 from framework.params.eal import EalParams
 from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
 
-from .cpu import LogicalCoreCount, LogicalCoreList
 from .node import Node
-from .os_session import InteractiveShellType, OSSession
-from .port import Port
-from .virtual_device import VirtualDevice
+from .os_session import OSSession
 
 
 class SutNode(Node):
@@ -56,8 +51,8 @@ class SutNode(Node):
     """
 
     config: SutNodeConfiguration
-    _dpdk_prefix_list: list[str]
-    _dpdk_timestamp: str
+    dpdk_prefix_list: list[str]
+    dpdk_timestamp: str
     _build_target_config: BuildTargetConfiguration | None
     _env_vars: dict
     _remote_tmp_dir: PurePath
@@ -76,14 +71,14 @@ def __init__(self, node_config: SutNodeConfiguration):
             node_config: The SUT node's test run configuration.
         """
         super(SutNode, self).__init__(node_config)
-        self._dpdk_prefix_list = []
+        self.dpdk_prefix_list = []
         self._build_target_config = None
         self._env_vars = {}
         self._remote_tmp_dir = self.main_session.get_remote_tmp_dir()
         self.__remote_dpdk_dir = None
         self._app_compile_timeout = 90
         self._dpdk_kill_session = None
-        self._dpdk_timestamp = (
+        self.dpdk_timestamp = (
             f"{str(os.getpid())}_{time.strftime('%Y%m%d%H%M%S', time.localtime())}"
         )
         self._dpdk_version = None
@@ -283,73 +278,11 @@ def kill_cleanup_dpdk_apps(self) -> None:
         """Kill all dpdk applications on the SUT, then clean up hugepages."""
         if self._dpdk_kill_session and self._dpdk_kill_session.is_alive():
             # we can use the session if it exists and responds
-            self._dpdk_kill_session.kill_cleanup_dpdk_apps(self._dpdk_prefix_list)
+            self._dpdk_kill_session.kill_cleanup_dpdk_apps(self.dpdk_prefix_list)
         else:
             # otherwise, we need to (re)create it
             self._dpdk_kill_session = self.create_session("dpdk_kill")
-        self._dpdk_prefix_list = []
-
-    def create_eal_parameters(
-        self,
-        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
-        ascending_cores: bool = True,
-        prefix: str = "dpdk",
-        append_prefix_timestamp: bool = True,
-        no_pci: Switch = None,
-        vdevs: list[VirtualDevice] | None = None,
-        ports: list[Port] | None = None,
-        other_eal_param: str = "",
-    ) -> EalParams:
-        """Compose the EAL parameters.
-
-        Process the list of cores and the DPDK prefix and pass that along with
-        the rest of the arguments.
-
-        Args:
-            lcore_filter_specifier: A number of lcores/cores/sockets to use
-                or a list of lcore ids to use.
-                The default will select one lcore for each of two cores
-                on one socket, in ascending order of core ids.
-            ascending_cores: Sort cores in ascending order (lowest to highest IDs).
-                If :data:`False`, sort in descending order.
-            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
-            append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.
-            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
-            vdevs: Virtual devices, e.g.::
-
-                vdevs=[
-                    VirtualDevice('net_ring0'),
-                    VirtualDevice('net_ring1')
-                ]
-            ports: The list of ports to allow. If :data:`None`, all ports listed in `self.ports`
-                will be allowed.
-            other_eal_param: user defined DPDK EAL parameters, e.g.:
-                ``other_eal_param='--single-file-segments'``.
-
-        Returns:
-            An EAL param string, such as
-            ``-c 0xf -a 0000:88:00.0 --file-prefix=dpdk_1112_20190809143420``.
-        """
-        lcore_list = LogicalCoreList(self.filter_lcores(lcore_filter_specifier, ascending_cores))
-
-        if append_prefix_timestamp:
-            prefix = f"{prefix}_{self._dpdk_timestamp}"
-        prefix = self.main_session.get_dpdk_file_prefix(prefix)
-        if prefix:
-            self._dpdk_prefix_list.append(prefix)
-
-        if ports is None:
-            ports = self.ports
-
-        return EalParams(
-            lcore_list=lcore_list,
-            memory_channels=self.config.memory_channels,
-            prefix=prefix,
-            no_pci=no_pci,
-            vdevs=vdevs,
-            ports=ports,
-            other_eal_param=Params.from_str(other_eal_param),
-        )
+        self.dpdk_prefix_list = []
 
     def run_dpdk_app(
         self, app_path: PurePath, eal_params: EalParams, timeout: float = 30
@@ -379,49 +312,6 @@ def configure_ipv4_forwarding(self, enable: bool) -> None:
         """
         self.main_session.configure_ipv4_forwarding(enable)
 
-    def create_interactive_shell(
-        self,
-        shell_cls: Type[InteractiveShellType],
-        timeout: float = SETTINGS.timeout,
-        privileged: bool = False,
-        app_params: Params = Params(),
-        eal_params: EalParams | None = None,
-    ) -> InteractiveShellType:
-        """Extend the factory for interactive session handlers.
-
-        The extensions are SUT node specific:
-
-            * The default for `eal_parameters`,
-            * The interactive shell path `shell_cls.path` is prepended with path to the remote
-              DPDK build directory for DPDK apps.
-
-        Args:
-            shell_cls: The class of the shell.
-            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.
-            privileged: Whether to run the shell with administrative privileges.
-            app_params: The parameters to be passed to the application.
-            eal_params: List of EAL parameters to use to launch the app. If this
-                isn't provided or an empty string is passed, it will default to calling
-                :meth:`create_eal_parameters`.
-
-        Returns:
-            An instance of the desired interactive application shell.
-        """
-        # We need to append the build directory and add EAL parameters for DPDK apps
-        if shell_cls.dpdk_app:
-            if eal_params is None:
-                eal_params = self.create_eal_parameters()
-            eal_params.append_str(str(app_params))
-            app_params = eal_params
-
-            shell_cls.path = self.main_session.join_remote_path(
-                self.remote_dpdk_build_dir, shell_cls.path
-            )
-
-        return super().create_interactive_shell(shell_cls, timeout, privileged, app_params)
-
     def bind_ports_to_driver(self, for_dpdk: bool = True) -> None:
         """Bind all ports on the SUT to a driver.
 
diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py
index 7bc1c2cc08..bf58ad1c5e 100644
--- a/dts/framework/testbed_model/traffic_generator/scapy.py
+++ b/dts/framework/testbed_model/traffic_generator/scapy.py
@@ -217,9 +217,7 @@ def __init__(self, tg_node: Node, config: ScapyTrafficGeneratorConfig):
             self._tg_node.config.os == OS.linux
         ), "Linux is the only supported OS for scapy traffic generation"
 
-        self.session = self._tg_node.create_interactive_shell(
-            PythonShell, timeout=5, privileged=True
-        )
+        self.session = PythonShell(self._tg_node, timeout=5, privileged=True)
 
         # import libs in remote python console
         for import_statement in SCAPY_RPC_SERVER_IMPORTS:
diff --git a/dts/tests/TestSuite_hello_world.py b/dts/tests/TestSuite_hello_world.py
index 0d6995f260..d958f99030 100644
--- a/dts/tests/TestSuite_hello_world.py
+++ b/dts/tests/TestSuite_hello_world.py
@@ -7,6 +7,7 @@
 No other EAL parameters apart from cores are used.
 """
 
+from framework.remote_session.dpdk_shell import compute_eal_params
 from framework.test_suite import TestSuite
 from framework.testbed_model.cpu import (
     LogicalCoreCount,
@@ -38,7 +39,7 @@ def test_hello_world_single_core(self) -> None:
         # get the first usable core
         lcore_amount = LogicalCoreCount(1, 1, 1)
         lcores = LogicalCoreCountFilter(self.sut_node.lcores, lcore_amount).filter()
-        eal_para = self.sut_node.create_eal_parameters(lcore_filter_specifier=lcore_amount)
+        eal_para = compute_eal_params(self.sut_node, lcore_filter_specifier=lcore_amount)
         result = self.sut_node.run_dpdk_app(self.app_helloworld_path, eal_para)
         self.verify(
             f"hello from core {int(lcores[0])}" in result.stdout,
@@ -55,8 +56,8 @@ def test_hello_world_all_cores(self) -> None:
             "hello from core <core_id>"
         """
         # get the maximum logical core number
-        eal_para = self.sut_node.create_eal_parameters(
-            lcore_filter_specifier=LogicalCoreList(self.sut_node.lcores)
+        eal_para = compute_eal_params(
+            self.sut_node, lcore_filter_specifier=LogicalCoreList(self.sut_node.lcores)
         )
         result = self.sut_node.run_dpdk_app(self.app_helloworld_path, eal_para, 50)
         for lcore in self.sut_node.lcores:
diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index 6d206c1a40..43cf5c61eb 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -16,14 +16,13 @@
 """
 
 import struct
-from dataclasses import asdict
 
 from scapy.layers.inet import IP  # type: ignore[import-untyped]
 from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
 from scapy.packet import Raw  # type: ignore[import-untyped]
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
-from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
+from framework.params.testpmd import SimpleForwardingModes
 from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.test_suite import TestSuite
 
@@ -103,17 +102,13 @@ def pmd_scatter(self, mbsize: int) -> None:
         Test:
             Start testpmd and run functional test with preset mbsize.
         """
-        testpmd = self.sut_node.create_interactive_shell(
-            TestPmdShell,
-            app_params=TestPmdParams(
-                forward_mode=SimpleForwardingModes.mac,
-                mbcache=200,
-                mbuf_size=[mbsize],
-                max_pkt_len=9000,
-                tx_offloads=0x00008000,
-                **asdict(self.sut_node.create_eal_parameters()),
-            ),
-            privileged=True,
+        testpmd = TestPmdShell(
+            self.sut_node,
+            forward_mode=SimpleForwardingModes.mac,
+            mbcache=200,
+            mbuf_size=[mbsize],
+            max_pkt_len=9000,
+            tx_offloads=0x00008000,
         )
         testpmd.start()
 
diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py
index ca678f662d..eca27acfd8 100644
--- a/dts/tests/TestSuite_smoke_tests.py
+++ b/dts/tests/TestSuite_smoke_tests.py
@@ -99,7 +99,7 @@ def test_devices_listed_in_testpmd(self) -> None:
         Test:
             List all devices found in testpmd and verify the configured devices are among them.
         """
-        testpmd_driver = self.sut_node.create_interactive_shell(TestPmdShell, privileged=True)
+        testpmd_driver = TestPmdShell(self.sut_node)
         dev_list = [str(x) for x in testpmd_driver.get_devices()]
         for nic in self.nics_in_node:
             self.verify(
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v5 8/8] dts: use Unpack for type checking and hinting
  2024-06-17 14:54 ` [PATCH v5 0/8] dts: add testpmd params Luca Vizzarro
                     ` (6 preceding siblings ...)
  2024-06-17 14:54   ` [PATCH v5 7/8] dts: rework interactive shells Luca Vizzarro
@ 2024-06-17 14:54   ` Luca Vizzarro
  2024-06-17 15:25     ` Nicholas Pratte
  7 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-17 14:54 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Interactive shells that inherit DPDKShell initialise their params
classes from a kwargs dict. Therefore, static type checking is
disabled. This change uses the functionality of Unpack added in
PEP 692 to re-enable it. The disadvantage is that this functionality has
been implemented only with TypedDict, forcing the creation of TypedDict
mirrors of the Params classes.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 dts/framework/params/types.py                 | 133 ++++++++++++++++++
 dts/framework/remote_session/testpmd_shell.py |   5 +-
 2 files changed, 136 insertions(+), 2 deletions(-)
 create mode 100644 dts/framework/params/types.py

diff --git a/dts/framework/params/types.py b/dts/framework/params/types.py
new file mode 100644
index 0000000000..e668f658d8
--- /dev/null
+++ b/dts/framework/params/types.py
@@ -0,0 +1,133 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Module containing TypeDict-equivalents of Params classes for static typing and hinting.
+
+TypedDicts can be used in conjunction with Unpack and kwargs for type hinting on function calls.
+
+Example:
+    ..code:: python
+        def create_testpmd(**kwargs: Unpack[TestPmdParamsDict]):
+            params = TestPmdParams(**kwargs)
+"""
+
+from pathlib import PurePath
+from typing import TypedDict
+
+from framework.params import Switch, YesNoSwitch
+from framework.params.testpmd import (
+    AnonMempoolAllocationMode,
+    EthPeer,
+    Event,
+    FlowGenForwardingMode,
+    HairpinMode,
+    NoisyForwardingMode,
+    Params,
+    PortNUMAConfig,
+    PortTopology,
+    RingNUMAConfig,
+    RSSSetting,
+    RXMultiQueueMode,
+    RXRingParams,
+    SimpleForwardingModes,
+    SimpleMempoolAllocationMode,
+    TxIPAddrPair,
+    TXOnlyForwardingMode,
+    TXRingParams,
+    TxUDPPortPair,
+)
+from framework.testbed_model.cpu import LogicalCoreList
+from framework.testbed_model.port import Port
+from framework.testbed_model.virtual_device import VirtualDevice
+
+
+class EalParamsDict(TypedDict, total=False):
+    """:class:`TypedDict` equivalent of :class:`~.eal.EalParams`."""
+
+    lcore_list: LogicalCoreList | None
+    memory_channels: int | None
+    prefix: str
+    no_pci: Switch
+    vdevs: list[VirtualDevice] | None
+    ports: list[Port] | None
+    other_eal_param: Params | None
+
+
+class TestPmdParamsDict(EalParamsDict, total=False):
+    """:class:`TypedDict` equivalent of :class:`~.testpmd.TestPmdParams`."""
+
+    interactive_mode: Switch
+    auto_start: Switch
+    tx_first: Switch
+    stats_period: int | None
+    display_xstats: list[str] | None
+    nb_cores: int | None
+    coremask: int | None
+    nb_ports: int | None
+    port_topology: PortTopology | None
+    portmask: int | None
+    portlist: str | None
+    numa: YesNoSwitch
+    socket_num: int | None
+    port_numa_config: list[PortNUMAConfig] | None
+    ring_numa_config: list[RingNUMAConfig] | None
+    total_num_mbufs: int | None
+    mbuf_size: list[int] | None
+    mbcache: int | None
+    max_pkt_len: int | None
+    eth_peers_configfile: PurePath | None
+    eth_peer: list[EthPeer] | None
+    tx_ip: TxIPAddrPair | None
+    tx_udp: TxUDPPortPair | None
+    enable_lro: Switch
+    max_lro_pkt_size: int | None
+    disable_crc_strip: Switch
+    enable_scatter: Switch
+    enable_hw_vlan: Switch
+    enable_hw_vlan_filter: Switch
+    enable_hw_vlan_strip: Switch
+    enable_hw_vlan_extend: Switch
+    enable_hw_qinq_strip: Switch
+    pkt_drop_enabled: Switch
+    rss: RSSSetting | None
+    forward_mode: (
+        SimpleForwardingModes
+        | FlowGenForwardingMode
+        | TXOnlyForwardingMode
+        | NoisyForwardingMode
+        | None
+    )
+    hairpin_mode: HairpinMode | None
+    hairpin_queues: int | None
+    burst: int | None
+    enable_rx_cksum: Switch
+    rx_queues: int | None
+    rx_ring: RXRingParams | None
+    no_flush_rx: Switch
+    rx_segments_offsets: list[int] | None
+    rx_segments_length: list[int] | None
+    multi_rx_mempool: Switch
+    rx_shared_queue: Switch | int
+    rx_offloads: int | None
+    rx_mq_mode: RXMultiQueueMode | None
+    tx_queues: int | None
+    tx_ring: TXRingParams | None
+    tx_offloads: int | None
+    eth_link_speed: int | None
+    disable_link_check: Switch
+    disable_device_start: Switch
+    no_lsc_interrupt: Switch
+    no_rmv_interrupt: Switch
+    bitrate_stats: int | None
+    latencystats: int | None
+    print_events: list[Event] | None
+    mask_events: list[Event] | None
+    flow_isolate_all: Switch
+    disable_flow_flush: Switch
+    hot_plug: Switch
+    vxlan_gpe_port: int | None
+    geneve_parsed_port: int | None
+    lock_all_memory: YesNoSwitch
+    mempool_allocation_mode: SimpleMempoolAllocationMode | AnonMempoolAllocationMode | None
+    record_core_cycles: Switch
+    record_burst_status: Switch
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index 8ee6829067..96a690b6de 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -21,10 +21,11 @@
 from pathlib import PurePath
 from typing import ClassVar
 
-from typing_extensions import Self
+from typing_extensions import Self, Unpack
 
 from framework.exception import InteractiveCommandExecutionError
 from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
+from framework.params.types import TestPmdParamsDict
 from framework.parser import ParserFn, TextParser
 from framework.remote_session.dpdk_shell import DPDKShell
 from framework.settings import SETTINGS
@@ -604,7 +605,7 @@ def __init__(
         ascending_cores: bool = True,
         append_prefix_timestamp: bool = True,
         start_on_init: bool = True,
-        **app_params,
+        **app_params: Unpack[TestPmdParamsDict],
     ) -> None:
         """Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs."""
         super().__init__(
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v5 1/8] dts: add params manipulation module
  2024-06-17 14:54   ` [PATCH v5 1/8] dts: add params manipulation module Luca Vizzarro
@ 2024-06-17 15:22     ` Nicholas Pratte
  0 siblings, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-06-17 15:22 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Jeremy Spewock, Juraj Linkeš, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Mon, Jun 17, 2024 at 10:54 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> This commit introduces a new "params" module, which adds a new way
> to manage command line parameters. The provided Params dataclass
> is able to read the fields of its child class and produce a string
> representation to supply to the command line. Any data structure
> that is intended to represent command line parameters can inherit it.
>
> The main purpose is to make it easier to represent data structures that
> map to parameters. Aiding quicker development, while minimising code
> bloat.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>  dts/framework/params/__init__.py | 358 +++++++++++++++++++++++++++++++
>  1 file changed, 358 insertions(+)
>  create mode 100644 dts/framework/params/__init__.py
>
> diff --git a/dts/framework/params/__init__.py b/dts/framework/params/__init__.py
> new file mode 100644
> index 0000000000..107b070ed2
> --- /dev/null
> +++ b/dts/framework/params/__init__.py
> @@ -0,0 +1,358 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2024 Arm Limited
> +
> +"""Parameter manipulation module.
> +
> +This module provides :class:`Params` which can be used to model any data structure
> +that is meant to represent any command line parameters.
> +"""
> +
> +from dataclasses import dataclass, fields
> +from enum import Flag
> +from typing import (
> +    Any,
> +    Callable,
> +    Iterable,
> +    Literal,
> +    Reversible,
> +    TypedDict,
> +    TypeVar,
> +    cast,
> +)
> +
> +from typing_extensions import Self
> +
> +T = TypeVar("T")
> +
> +#: Type for a function taking one argument.
> +FnPtr = Callable[[Any], Any]
> +#: Type for a switch parameter.
> +Switch = Literal[True, None]
> +#: Type for a yes/no switch parameter.
> +YesNoSwitch = Literal[True, False, None]
> +
> +
> +def _reduce_functions(funcs: Iterable[FnPtr]) -> FnPtr:
> +    """Reduces an iterable of :attr:`FnPtr` from left to right to a single function.
> +
> +    If the iterable is empty, the created function just returns its fed value back.
> +
> +    Args:
> +        funcs: An iterable containing the functions to be chained from left to right.
> +
> +    Returns:
> +        FnPtr: A function that calls the given functions from left to right.
> +    """
> +
> +    def reduced_fn(value):
> +        for fn in funcs:
> +            value = fn(value)
> +        return value
> +
> +    return reduced_fn
> +
> +
> +def modify_str(*funcs: FnPtr) -> Callable[[T], T]:
> +    """Class decorator modifying the ``__str__`` method with a function created from its arguments.
> +
> +    The :attr:`FnPtr`s fed to the decorator are executed from left to right in the arguments list
> +    order.
> +
> +    Args:
> +        *funcs: The functions to chain from left to right.
> +
> +    Returns:
> +        The decorator.
> +
> +    Example:
> +        .. code:: python
> +
> +            @convert_str(hex_from_flag_value)
> +            class BitMask(enum.Flag):
> +                A = auto()
> +                B = auto()
> +
> +        will allow ``BitMask`` to render as a hexadecimal value.
> +    """
> +
> +    def _class_decorator(original_class):
> +        original_class.__str__ = _reduce_functions(funcs)
> +        return original_class
> +
> +    return _class_decorator
> +
> +
> +def comma_separated(values: Iterable[Any]) -> str:
> +    """Converts an iterable into a comma-separated string.
> +
> +    Args:
> +        values: An iterable of objects.
> +
> +    Returns:
> +        A comma-separated list of stringified values.
> +    """
> +    return ",".join([str(value).strip() for value in values if value is not None])
> +
> +
> +def bracketed(value: str) -> str:
> +    """Adds round brackets to the input.
> +
> +    Args:
> +        value: Any string.
> +
> +    Returns:
> +        A string surrounded by round brackets.
> +    """
> +    return f"({value})"
> +
> +
> +def str_from_flag_value(flag: Flag) -> str:
> +    """Returns the value from a :class:`enum.Flag` as a string.
> +
> +    Args:
> +        flag: An instance of :class:`Flag`.
> +
> +    Returns:
> +        The stringified value of the given flag.
> +    """
> +    return str(flag.value)
> +
> +
> +def hex_from_flag_value(flag: Flag) -> str:
> +    """Returns the value from a :class:`enum.Flag` converted to hexadecimal.
> +
> +    Args:
> +        flag: An instance of :class:`Flag`.
> +
> +    Returns:
> +        The value of the given flag in hexadecimal representation.
> +    """
> +    return hex(flag.value)
> +
> +
> +class ParamsModifier(TypedDict, total=False):
> +    """Params modifiers dict compatible with the :func:`dataclasses.field` metadata parameter."""
> +
> +    #:
> +    Params_short: str
> +    #:
> +    Params_long: str
> +    #:
> +    Params_multiple: bool
> +    #:
> +    Params_convert_value: Reversible[FnPtr]
> +
> +
> +@dataclass
> +class Params:
> +    """Dataclass that renders its fields into command line arguments.
> +
> +    The parameter name is taken from the field name by default. The following:
> +
> +    .. code:: python
> +
> +        name: str | None = "value"
> +
> +    is rendered as ``--name=value``.
> +    Through :func:`dataclasses.field` the resulting parameter can be manipulated by applying
> +    this class' metadata modifier functions. These return regular dictionaries which can be combined
> +    together using the pipe (OR) operator.
> +
> +    To use fields as switches, set the value to ``True`` to render them. If you
> +    use a yes/no switch you can also set ``False`` which would render a switch
> +    prefixed with ``--no-``. Examples:
> +
> +    .. code:: python
> +
> +        interactive: Switch = True  # renders --interactive
> +        numa: YesNoSwitch   = False # renders --no-numa
> +
> +    Setting ``None`` will prevent it from being rendered. The :attr:`~Switch` type alias is provided
> +    for regular switches, whereas :attr:`~YesNoSwitch` is offered for yes/no ones.
> +
> +    An instance of a dataclass inheriting ``Params`` can also be assigned to an attribute,
> +    this helps with grouping parameters together.
> +    The attribute holding the dataclass will be ignored and the latter will just be rendered as
> +    expected.
> +    """
> +
> +    _suffix = ""
> +    """Holder of the plain text value of Params when called directly. A suffix for child classes."""
> +
> +    """========= BEGIN FIELD METADATA MODIFIER FUNCTIONS ========"""
> +
> +    @staticmethod
> +    def short(name: str) -> ParamsModifier:
> +        """Overrides any parameter name with the given short option.
> +
> +        Args:
> +            name: The short parameter name.
> +
> +        Returns:
> +            ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
> +                the parameter short name modifier.
> +
> +        Example:
> +            .. code:: python
> +
> +                logical_cores: str | None = field(default="1-4", metadata=Params.short("l"))
> +
> +            will render as ``-l=1-4`` instead of ``--logical-cores=1-4``.
> +        """
> +        return ParamsModifier(Params_short=name)
> +
> +    @staticmethod
> +    def long(name: str) -> ParamsModifier:
> +        """Overrides the inferred parameter name to the specified one.
> +
> +        Args:
> +            name: The long parameter name.
> +
> +        Returns:
> +            ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
> +                the parameter long name modifier.
> +
> +        Example:
> +            .. code:: python
> +
> +                x_name: str | None = field(default="y", metadata=Params.long("x"))
> +
> +            will render as ``--x=y``, but the field is accessed and modified through ``x_name``.
> +        """
> +        return ParamsModifier(Params_long=name)
> +
> +    @staticmethod
> +    def multiple() -> ParamsModifier:
> +        """Specifies that this parameter is set multiple times. The parameter type must be a list.
> +
> +        Returns:
> +            ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
> +                the multiple parameters modifier.
> +
> +        Example:
> +            .. code:: python
> +
> +                ports: list[int] | None = field(
> +                    default_factory=lambda: [0, 1, 2],
> +                    metadata=Params.multiple() | Params.long("port")
> +                )
> +
> +            will render as ``--port=0 --port=1 --port=2``.
> +        """
> +        return ParamsModifier(Params_multiple=True)
> +
> +    @staticmethod
> +    def convert_value(*funcs: FnPtr) -> ParamsModifier:
> +        """Takes in a variable number of functions to convert the value text representation.
> +
> +        Functions can be chained together, executed from left to right in the arguments list order.
> +
> +        Args:
> +            *funcs: The functions to chain from left to right.
> +
> +        Returns:
> +            ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
> +                the convert value modifier.
> +
> +        Example:
> +            .. code:: python
> +
> +                hex_bitmask: int | None = field(
> +                    default=0b1101,
> +                    metadata=Params.convert_value(hex) | Params.long("mask")
> +                )
> +
> +            will render as ``--mask=0xd``.
> +        """
> +        return ParamsModifier(Params_convert_value=funcs)
> +
> +    """========= END FIELD METADATA MODIFIER FUNCTIONS ========"""
> +
> +    def append_str(self, text: str) -> None:
> +        """Appends a string at the end of the string representation.
> +
> +        Args:
> +            text: Any text to append at the end of the parameters string representation.
> +        """
> +        self._suffix += text
> +
> +    def __iadd__(self, text: str) -> Self:
> +        """Appends a string at the end of the string representation.
> +
> +        Args:
> +            text: Any text to append at the end of the parameters string representation.
> +
> +        Returns:
> +            The given instance back.
> +        """
> +        self.append_str(text)
> +        return self
> +
> +    @classmethod
> +    def from_str(cls, text: str) -> Self:
> +        """Creates a plain Params object from a string.
> +
> +        Args:
> +            text: The string parameters.
> +
> +        Returns:
> +            A new plain instance of :class:`Params`.
> +        """
> +        obj = cls()
> +        obj.append_str(text)
> +        return obj
> +
> +    @staticmethod
> +    def _make_switch(
> +        name: str, is_short: bool = False, is_no: bool = False, value: str | None = None
> +    ) -> str:
> +        """Make the string representation of the parameter.
> +
> +        Args:
> +            name: The name of the parameters.
> +            is_short: If the parameters is short or not.
> +            is_no: If the parameter is negated or not.
> +            value: The value of the parameter.
> +
> +        Returns:
> +            The complete command line parameter.
> +        """
> +        prefix = f"{'-' if is_short else '--'}{'no-' if is_no else ''}"
> +        name = name.replace("_", "-")
> +        value = f"{' ' if is_short else '='}{value}" if value else ""
> +        return f"{prefix}{name}{value}"
> +
> +    def __str__(self) -> str:
> +        """Returns a string of command-line-ready arguments from the class fields."""
> +        arguments: list[str] = []
> +
> +        for field in fields(self):
> +            value = getattr(self, field.name)
> +            modifiers = cast(ParamsModifier, field.metadata)
> +
> +            if value is None:
> +                continue
> +
> +            if isinstance(value, Params):
> +                arguments.append(str(value))
> +                continue
> +
> +            # take the short modifier, or the long modifier, or infer from field name
> +            switch_name = modifiers.get("Params_short", modifiers.get("Params_long", field.name))
> +            is_short = "Params_short" in modifiers
> +
> +            if isinstance(value, bool):
> +                arguments.append(self._make_switch(switch_name, is_short, is_no=(not value)))
> +                continue
> +
> +            convert = _reduce_functions(modifiers.get("Params_convert_value", []))
> +            multiple = modifiers.get("Params_multiple", False)
> +
> +            values = value if multiple else [value]
> +            for value in values:
> +                arguments.append(self._make_switch(switch_name, is_short, value=convert(value)))
> +
> +        if self._suffix:
> +            arguments.append(self._suffix)
> +
> +        return " ".join(arguments)
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v5 2/8] dts: use Params for interactive shells
  2024-06-17 14:54   ` [PATCH v5 2/8] dts: use Params for interactive shells Luca Vizzarro
@ 2024-06-17 15:23     ` Nicholas Pratte
  0 siblings, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-06-17 15:23 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Jeremy Spewock, Juraj Linkeš, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Mon, Jun 17, 2024 at 10:54 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Make it so that interactive shells accept an implementation of `Params`
> for app arguments. Convert EalParameters to use `Params` instead.
>
> String command line parameters can still be supplied by using the
> `Params.from_str()` method.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
> Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
> Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
> ---
>  .../remote_session/interactive_shell.py       |  12 +-
>  dts/framework/remote_session/testpmd_shell.py |  11 +-
>  dts/framework/testbed_model/node.py           |   6 +-
>  dts/framework/testbed_model/os_session.py     |   4 +-
>  dts/framework/testbed_model/sut_node.py       | 124 ++++++++----------
>  dts/tests/TestSuite_pmd_buffer_scatter.py     |   3 +-
>  6 files changed, 77 insertions(+), 83 deletions(-)
>
> diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
> index c025c52ba3..8191b36630 100644
> --- a/dts/framework/remote_session/interactive_shell.py
> +++ b/dts/framework/remote_session/interactive_shell.py
> @@ -1,5 +1,6 @@
>  # SPDX-License-Identifier: BSD-3-Clause
>  # Copyright(c) 2023 University of New Hampshire
> +# Copyright(c) 2024 Arm Limited
>
>  """Common functionality for interactive shell handling.
>
> @@ -21,6 +22,7 @@
>  from paramiko import Channel, SSHClient, channel  # type: ignore[import-untyped]
>
>  from framework.logger import DTSLogger
> +from framework.params import Params
>  from framework.settings import SETTINGS
>
>
> @@ -40,7 +42,7 @@ class InteractiveShell(ABC):
>      _ssh_channel: Channel
>      _logger: DTSLogger
>      _timeout: float
> -    _app_args: str
> +    _app_params: Params
>
>      #: Prompt to expect at the end of output when sending a command.
>      #: This is often overridden by subclasses.
> @@ -63,7 +65,7 @@ def __init__(
>          interactive_session: SSHClient,
>          logger: DTSLogger,
>          get_privileged_command: Callable[[str], str] | None,
> -        app_args: str = "",
> +        app_params: Params = Params(),
>          timeout: float = SETTINGS.timeout,
>      ) -> None:
>          """Create an SSH channel during initialization.
> @@ -74,7 +76,7 @@ def __init__(
>              get_privileged_command: A method for modifying a command to allow it to use
>                  elevated privileges. If :data:`None`, the application will not be started
>                  with elevated privileges.
> -            app_args: The command line arguments to be passed to the application on startup.
> +            app_params: The command line parameters to be passed to the application on startup.
>              timeout: The timeout used for the SSH channel that is dedicated to this interactive
>                  shell. This timeout is for collecting output, so if reading from the buffer
>                  and no output is gathered within the timeout, an exception is thrown.
> @@ -87,7 +89,7 @@ def __init__(
>          self._ssh_channel.set_combine_stderr(True)  # combines stdout and stderr streams
>          self._logger = logger
>          self._timeout = timeout
> -        self._app_args = app_args
> +        self._app_params = app_params
>          self._start_application(get_privileged_command)
>
>      def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
> @@ -100,7 +102,7 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
>              get_privileged_command: A function (but could be any callable) that produces
>                  the version of the command with elevated privileges.
>          """
> -        start_command = f"{self.path} {self._app_args}"
> +        start_command = f"{self.path} {self._app_params}"
>          if get_privileged_command is not None:
>              start_command = get_privileged_command(start_command)
>          self.send_command(start_command)
> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
> index d413bf2cc7..2836ed5c48 100644
> --- a/dts/framework/remote_session/testpmd_shell.py
> +++ b/dts/framework/remote_session/testpmd_shell.py
> @@ -28,6 +28,7 @@
>  from framework.exception import InteractiveCommandExecutionError
>  from framework.parser import ParserFn, TextParser
>  from framework.settings import SETTINGS
> +from framework.testbed_model.sut_node import EalParams
>  from framework.utils import StrEnum
>
>  from .interactive_shell import InteractiveShell
> @@ -645,8 +646,14 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
>          Also find the number of pci addresses which were allowed on the command line when the app
>          was started.
>          """
> -        self._app_args += " -i --mask-event intr_lsc"
> -        self.number_of_ports = self._app_args.count("-a ")
> +        self._app_params += " -i --mask-event intr_lsc"
> +
> +        assert isinstance(self._app_params, EalParams)
> +
> +        self.number_of_ports = (
> +            len(self._app_params.ports) if self._app_params.ports is not None else 0
> +        )
> +
>          super()._start_application(get_privileged_command)
>
>      def start(self, verify: bool = True) -> None:
> diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
> index 74061f6262..6af4f25a3c 100644
> --- a/dts/framework/testbed_model/node.py
> +++ b/dts/framework/testbed_model/node.py
> @@ -2,6 +2,7 @@
>  # Copyright(c) 2010-2014 Intel Corporation
>  # Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
>  # Copyright(c) 2022-2023 University of New Hampshire
> +# Copyright(c) 2024 Arm Limited
>
>  """Common functionality for node management.
>
> @@ -24,6 +25,7 @@
>  )
>  from framework.exception import ConfigurationError
>  from framework.logger import DTSLogger, get_dts_logger
> +from framework.params import Params
>  from framework.settings import SETTINGS
>
>  from .cpu import (
> @@ -199,7 +201,7 @@ def create_interactive_shell(
>          shell_cls: Type[InteractiveShellType],
>          timeout: float = SETTINGS.timeout,
>          privileged: bool = False,
> -        app_args: str = "",
> +        app_params: Params = Params(),
>      ) -> InteractiveShellType:
>          """Factory for interactive session handlers.
>
> @@ -222,7 +224,7 @@ def create_interactive_shell(
>              shell_cls,
>              timeout,
>              privileged,
> -            app_args,
> +            app_params,
>          )
>
>      def filter_lcores(
> diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
> index d5bf7e0401..1a77aee532 100644
> --- a/dts/framework/testbed_model/os_session.py
> +++ b/dts/framework/testbed_model/os_session.py
> @@ -1,6 +1,7 @@
>  # SPDX-License-Identifier: BSD-3-Clause
>  # Copyright(c) 2023 PANTHEON.tech s.r.o.
>  # Copyright(c) 2023 University of New Hampshire
> +# Copyright(c) 2024 Arm Limited
>
>  """OS-aware remote session.
>
> @@ -29,6 +30,7 @@
>
>  from framework.config import Architecture, NodeConfiguration, NodeInfo
>  from framework.logger import DTSLogger
> +from framework.params import Params
>  from framework.remote_session import (
>      CommandResult,
>      InteractiveRemoteSession,
> @@ -134,7 +136,7 @@ def create_interactive_shell(
>          shell_cls: Type[InteractiveShellType],
>          timeout: float,
>          privileged: bool,
> -        app_args: str,
> +        app_args: Params,
>      ) -> InteractiveShellType:
>          """Factory for interactive session handlers.
>
> diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
> index 97aa26d419..c886590979 100644
> --- a/dts/framework/testbed_model/sut_node.py
> +++ b/dts/framework/testbed_model/sut_node.py
> @@ -2,6 +2,7 @@
>  # Copyright(c) 2010-2014 Intel Corporation
>  # Copyright(c) 2023 PANTHEON.tech s.r.o.
>  # Copyright(c) 2023 University of New Hampshire
> +# Copyright(c) 2024 Arm Limited
>
>  """System under test (DPDK + hardware) node.
>
> @@ -14,8 +15,9 @@
>  import os
>  import tarfile
>  import time
> +from dataclasses import dataclass, field
>  from pathlib import PurePath
> -from typing import Type
> +from typing import Literal, Type
>
>  from framework.config import (
>      BuildTargetConfiguration,
> @@ -23,6 +25,7 @@
>      NodeInfo,
>      SutNodeConfiguration,
>  )
> +from framework.params import Params, Switch
>  from framework.remote_session import CommandResult
>  from framework.settings import SETTINGS
>  from framework.utils import MesonArgs
> @@ -34,62 +37,42 @@
>  from .virtual_device import VirtualDevice
>
>
> -class EalParameters(object):
> -    """The environment abstraction layer parameters.
> -
> -    The string representation can be created by converting the instance to a string.
> -    """
> +def _port_to_pci(port: Port) -> str:
> +    return port.pci
>
> -    def __init__(
> -        self,
> -        lcore_list: LogicalCoreList,
> -        memory_channels: int,
> -        prefix: str,
> -        no_pci: bool,
> -        vdevs: list[VirtualDevice],
> -        ports: list[Port],
> -        other_eal_param: str,
> -    ):
> -        """Initialize the parameters according to inputs.
> -
> -        Process the parameters into the format used on the command line.
>
> -        Args:
> -            lcore_list: The list of logical cores to use.
> -            memory_channels: The number of memory channels to use.
> -            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
> -            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
> -            vdevs: Virtual devices, e.g.::
> +@dataclass(kw_only=True)
> +class EalParams(Params):
> +    """The environment abstraction layer parameters.
>
> -                vdevs=[
> -                    VirtualDevice('net_ring0'),
> -                    VirtualDevice('net_ring1')
> -                ]
> -            ports: The list of ports to allow.
> -            other_eal_param: user defined DPDK EAL parameters, e.g.:
> +    Attributes:
> +        lcore_list: The list of logical cores to use.
> +        memory_channels: The number of memory channels to use.
> +        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
> +        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
> +        vdevs: Virtual devices, e.g.::
> +            vdevs=[
> +                VirtualDevice('net_ring0'),
> +                VirtualDevice('net_ring1')
> +            ]
> +        ports: The list of ports to allow.
> +        other_eal_param: user defined DPDK EAL parameters, e.g.:
>                  ``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._ports = " ".join(f"-a {port.pci}" for port in ports)
> -        self._other_eal_param = other_eal_param
> -
> -    def __str__(self) -> str:
> -        """Create the EAL string."""
> -        return (
> -            f"{self._lcore_list} "
> -            f"{self._memory_channels} "
> -            f"{self._prefix} "
> -            f"{self._no_pci} "
> -            f"{self._vdevs} "
> -            f"{self._ports} "
> -            f"{self._other_eal_param}"
> -        )
> +    """
> +
> +    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
> +    memory_channels: int = field(metadata=Params.short("n"))
> +    prefix: str = field(metadata=Params.long("file-prefix"))
> +    no_pci: Switch
> +    vdevs: list[VirtualDevice] | None = field(
> +        default=None, metadata=Params.multiple() | Params.long("vdev")
> +    )
> +    ports: list[Port] | None = field(
> +        default=None,
> +        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
> +    )
> +    other_eal_param: Params | None = None
> +    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
>
>
>  class SutNode(Node):
> @@ -350,11 +333,11 @@ def create_eal_parameters(
>          ascending_cores: bool = True,
>          prefix: str = "dpdk",
>          append_prefix_timestamp: bool = True,
> -        no_pci: bool = False,
> +        no_pci: Switch = None,
>          vdevs: list[VirtualDevice] | None = None,
>          ports: list[Port] | None = None,
>          other_eal_param: str = "",
> -    ) -> "EalParameters":
> +    ) -> EalParams:
>          """Compose the EAL parameters.
>
>          Process the list of cores and the DPDK prefix and pass that along with
> @@ -393,24 +376,21 @@ def create_eal_parameters(
>          if prefix:
>              self._dpdk_prefix_list.append(prefix)
>
> -        if vdevs is None:
> -            vdevs = []
> -
>          if ports is None:
>              ports = self.ports
>
> -        return EalParameters(
> +        return EalParams(
>              lcore_list=lcore_list,
>              memory_channels=self.config.memory_channels,
>              prefix=prefix,
>              no_pci=no_pci,
>              vdevs=vdevs,
>              ports=ports,
> -            other_eal_param=other_eal_param,
> +            other_eal_param=Params.from_str(other_eal_param),
>          )
>
>      def run_dpdk_app(
> -        self, app_path: PurePath, eal_args: "EalParameters", timeout: float = 30
> +        self, app_path: PurePath, eal_params: EalParams, timeout: float = 30
>      ) -> CommandResult:
>          """Run DPDK application on the remote node.
>
> @@ -419,14 +399,14 @@ def run_dpdk_app(
>
>          Args:
>              app_path: The remote path to the DPDK application.
> -            eal_args: EAL parameters to run the DPDK application with.
> +            eal_params: EAL parameters to run the DPDK application with.
>              timeout: Wait at most this long in seconds for `command` execution to complete.
>
>          Returns:
>              The result of the DPDK app execution.
>          """
>          return self.main_session.send_command(
> -            f"{app_path} {eal_args}", timeout, privileged=True, verify=True
> +            f"{app_path} {eal_params}", timeout, privileged=True, verify=True
>          )
>
>      def configure_ipv4_forwarding(self, enable: bool) -> None:
> @@ -442,8 +422,8 @@ def create_interactive_shell(
>          shell_cls: Type[InteractiveShellType],
>          timeout: float = SETTINGS.timeout,
>          privileged: bool = False,
> -        app_parameters: str = "",
> -        eal_parameters: EalParameters | None = None,
> +        app_params: Params = Params(),
> +        eal_params: EalParams | None = None,
>      ) -> InteractiveShellType:
>          """Extend the factory for interactive session handlers.
>
> @@ -459,26 +439,26 @@ def create_interactive_shell(
>                  reading from the buffer and don't receive any data within the timeout
>                  it will throw an error.
>              privileged: Whether to run the shell with administrative privileges.
> -            eal_parameters: List of EAL parameters to use to launch the app. If this
> +            app_params: The parameters to be passed to the application.
> +            eal_params: List of EAL parameters to use to launch the app. If this
>                  isn't provided or an empty string is passed, it will default to calling
>                  :meth:`create_eal_parameters`.
> -            app_parameters: Additional arguments to pass into the application on the
> -                command-line.
>
>          Returns:
>              An instance of the desired interactive application shell.
>          """
>          # We need to append the build directory and add EAL parameters for DPDK apps
>          if shell_cls.dpdk_app:
> -            if not eal_parameters:
> -                eal_parameters = self.create_eal_parameters()
> -            app_parameters = f"{eal_parameters} -- {app_parameters}"
> +            if eal_params is None:
> +                eal_params = self.create_eal_parameters()
> +            eal_params.append_str(str(app_params))
> +            app_params = eal_params
>
>              shell_cls.path = self.main_session.join_remote_path(
>                  self.remote_dpdk_build_dir, shell_cls.path
>              )
>
> -        return super().create_interactive_shell(shell_cls, timeout, privileged, app_parameters)
> +        return super().create_interactive_shell(shell_cls, timeout, privileged, app_params)
>
>      def bind_ports_to_driver(self, for_dpdk: bool = True) -> None:
>          """Bind all ports on the SUT to a driver.
> diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
> index a020682e8d..c6e93839cb 100644
> --- a/dts/tests/TestSuite_pmd_buffer_scatter.py
> +++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
> @@ -22,6 +22,7 @@
>  from scapy.packet import Raw  # type: ignore[import-untyped]
>  from scapy.utils import hexstr  # type: ignore[import-untyped]
>
> +from framework.params import Params
>  from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
>  from framework.test_suite import TestSuite
>
> @@ -103,7 +104,7 @@ def pmd_scatter(self, mbsize: int) -> None:
>          """
>          testpmd = self.sut_node.create_interactive_shell(
>              TestPmdShell,
> -            app_parameters=(
> +            app_params=Params.from_str(
>                  "--mbcache=200 "
>                  f"--mbuf-size={mbsize} "
>                  "--max-pkt-len=9000 "
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v5 3/8] dts: refactor EalParams
  2024-06-17 14:54   ` [PATCH v5 3/8] dts: refactor EalParams Luca Vizzarro
@ 2024-06-17 15:23     ` Nicholas Pratte
  0 siblings, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-06-17 15:23 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Jeremy Spewock, Juraj Linkeš, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Mon, Jun 17, 2024 at 10:54 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Move EalParams to its own module to avoid circular dependencies.
> Also the majority of the attributes are now optional.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
> Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
> Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
> ---
>  dts/framework/params/eal.py                   | 50 +++++++++++++++++++
>  dts/framework/remote_session/testpmd_shell.py |  2 +-
>  dts/framework/testbed_model/sut_node.py       | 42 +---------------
>  3 files changed, 53 insertions(+), 41 deletions(-)
>  create mode 100644 dts/framework/params/eal.py
>
> diff --git a/dts/framework/params/eal.py b/dts/framework/params/eal.py
> new file mode 100644
> index 0000000000..bbdbc8f334
> --- /dev/null
> +++ b/dts/framework/params/eal.py
> @@ -0,0 +1,50 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2024 Arm Limited
> +
> +"""Module representing the DPDK EAL-related parameters."""
> +
> +from dataclasses import dataclass, field
> +from typing import Literal
> +
> +from framework.params import Params, Switch
> +from framework.testbed_model.cpu import LogicalCoreList
> +from framework.testbed_model.port import Port
> +from framework.testbed_model.virtual_device import VirtualDevice
> +
> +
> +def _port_to_pci(port: Port) -> str:
> +    return port.pci
> +
> +
> +@dataclass(kw_only=True)
> +class EalParams(Params):
> +    """The environment abstraction layer parameters.
> +
> +    Attributes:
> +        lcore_list: The list of logical cores to use.
> +        memory_channels: The number of memory channels to use.
> +        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
> +        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
> +        vdevs: Virtual devices, e.g.::
> +            vdevs=[
> +                VirtualDevice('net_ring0'),
> +                VirtualDevice('net_ring1')
> +            ]
> +        ports: The list of ports to allow.
> +        other_eal_param: user defined DPDK EAL parameters, e.g.:
> +                ``other_eal_param='--single-file-segments'``
> +    """
> +
> +    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
> +    memory_channels: int = field(metadata=Params.short("n"))
> +    prefix: str = field(metadata=Params.long("file-prefix"))
> +    no_pci: Switch = None
> +    vdevs: list[VirtualDevice] | None = field(
> +        default=None, metadata=Params.multiple() | Params.long("vdev")
> +    )
> +    ports: list[Port] | None = field(
> +        default=None,
> +        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
> +    )
> +    other_eal_param: Params | None = None
> +    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
> index 2836ed5c48..2b9ef9418d 100644
> --- a/dts/framework/remote_session/testpmd_shell.py
> +++ b/dts/framework/remote_session/testpmd_shell.py
> @@ -26,9 +26,9 @@
>  from typing_extensions import Self
>
>  from framework.exception import InteractiveCommandExecutionError
> +from framework.params.eal import EalParams
>  from framework.parser import ParserFn, TextParser
>  from framework.settings import SETTINGS
> -from framework.testbed_model.sut_node import EalParams
>  from framework.utils import StrEnum
>
>  from .interactive_shell import InteractiveShell
> diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
> index c886590979..e1163106a3 100644
> --- a/dts/framework/testbed_model/sut_node.py
> +++ b/dts/framework/testbed_model/sut_node.py
> @@ -15,9 +15,8 @@
>  import os
>  import tarfile
>  import time
> -from dataclasses import dataclass, field
>  from pathlib import PurePath
> -from typing import Literal, Type
> +from typing import Type
>
>  from framework.config import (
>      BuildTargetConfiguration,
> @@ -26,6 +25,7 @@
>      SutNodeConfiguration,
>  )
>  from framework.params import Params, Switch
> +from framework.params.eal import EalParams
>  from framework.remote_session import CommandResult
>  from framework.settings import SETTINGS
>  from framework.utils import MesonArgs
> @@ -37,44 +37,6 @@
>  from .virtual_device import VirtualDevice
>
>
> -def _port_to_pci(port: Port) -> str:
> -    return port.pci
> -
> -
> -@dataclass(kw_only=True)
> -class EalParams(Params):
> -    """The environment abstraction layer parameters.
> -
> -    Attributes:
> -        lcore_list: The list of logical cores to use.
> -        memory_channels: The number of memory channels to use.
> -        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
> -        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
> -        vdevs: Virtual devices, e.g.::
> -            vdevs=[
> -                VirtualDevice('net_ring0'),
> -                VirtualDevice('net_ring1')
> -            ]
> -        ports: The list of ports to allow.
> -        other_eal_param: user defined DPDK EAL parameters, e.g.:
> -                ``other_eal_param='--single-file-segments'``
> -    """
> -
> -    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
> -    memory_channels: int = field(metadata=Params.short("n"))
> -    prefix: str = field(metadata=Params.long("file-prefix"))
> -    no_pci: Switch
> -    vdevs: list[VirtualDevice] | None = field(
> -        default=None, metadata=Params.multiple() | Params.long("vdev")
> -    )
> -    ports: list[Port] | None = field(
> -        default=None,
> -        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
> -    )
> -    other_eal_param: Params | None = None
> -    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
> -
> -
>  class SutNode(Node):
>      """The system under test node.
>
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v5 4/8] dts: remove module-wide imports
  2024-06-17 14:54   ` [PATCH v5 4/8] dts: remove module-wide imports Luca Vizzarro
@ 2024-06-17 15:23     ` Nicholas Pratte
  0 siblings, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-06-17 15:23 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Jeremy Spewock, Juraj Linkeš, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Mon, Jun 17, 2024 at 10:54 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Remove the imports in the testbed_model and remote_session modules init
> file, to avoid the initialisation of unneeded modules, thus removing or
> limiting the risk of circular dependencies.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
> Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
> Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
> ---
>  dts/framework/remote_session/__init__.py               | 7 +------
>  dts/framework/runner.py                                | 4 +++-
>  dts/framework/test_suite.py                            | 5 ++++-
>  dts/framework/testbed_model/__init__.py                | 9 ---------
>  dts/framework/testbed_model/os_session.py              | 4 ++--
>  dts/framework/testbed_model/sut_node.py                | 2 +-
>  dts/framework/testbed_model/traffic_generator/scapy.py | 2 +-
>  dts/tests/TestSuite_hello_world.py                     | 2 +-
>  dts/tests/TestSuite_smoke_tests.py                     | 2 +-
>  9 files changed, 14 insertions(+), 23 deletions(-)
>
> diff --git a/dts/framework/remote_session/__init__.py b/dts/framework/remote_session/__init__.py
> index 1910c81c3c..0668e9c884 100644
> --- a/dts/framework/remote_session/__init__.py
> +++ b/dts/framework/remote_session/__init__.py
> @@ -12,17 +12,12 @@
>  allowing it to send and receive data within that particular shell.
>  """
>
> -# pylama:ignore=W0611
> -
>  from framework.config import NodeConfiguration
>  from framework.logger import DTSLogger
>
>  from .interactive_remote_session import InteractiveRemoteSession
> -from .interactive_shell import InteractiveShell
> -from .python_shell import PythonShell
> -from .remote_session import CommandResult, RemoteSession
> +from .remote_session import RemoteSession
>  from .ssh_session import SSHSession
> -from .testpmd_shell import TestPmdShell
>
>
>  def create_remote_session(
> diff --git a/dts/framework/runner.py b/dts/framework/runner.py
> index dfdee14802..687bc04f79 100644
> --- a/dts/framework/runner.py
> +++ b/dts/framework/runner.py
> @@ -26,6 +26,9 @@
>  from types import FunctionType
>  from typing import Iterable, Sequence
>
> +from framework.testbed_model.sut_node import SutNode
> +from framework.testbed_model.tg_node import TGNode
> +
>  from .config import (
>      BuildTargetConfiguration,
>      Configuration,
> @@ -51,7 +54,6 @@
>      TestSuiteWithCases,
>  )
>  from .test_suite import TestSuite
> -from .testbed_model import SutNode, TGNode
>
>
>  class DTSRunner:
> diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py
> index 8768f756a6..9d3debb00f 100644
> --- a/dts/framework/test_suite.py
> +++ b/dts/framework/test_suite.py
> @@ -20,9 +20,12 @@
>  from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
>  from scapy.packet import Packet, Padding  # type: ignore[import-untyped]
>
> +from framework.testbed_model.port import Port, PortLink
> +from framework.testbed_model.sut_node import SutNode
> +from framework.testbed_model.tg_node import TGNode
> +
>  from .exception import TestCaseVerifyError
>  from .logger import DTSLogger, get_dts_logger
> -from .testbed_model import Port, PortLink, SutNode, TGNode
>  from .testbed_model.traffic_generator import PacketFilteringConfig
>  from .utils import get_packet_summaries
>
> diff --git a/dts/framework/testbed_model/__init__.py b/dts/framework/testbed_model/__init__.py
> index 6086512ca2..e3edd4d811 100644
> --- a/dts/framework/testbed_model/__init__.py
> +++ b/dts/framework/testbed_model/__init__.py
> @@ -17,12 +17,3 @@
>  DTS needs to be able to connect to nodes and understand some of the hardware present on these nodes
>  to properly build and test DPDK.
>  """
> -
> -# pylama:ignore=W0611
> -
> -from .cpu import LogicalCoreCount, LogicalCoreCountFilter, LogicalCoreList
> -from .node import Node
> -from .port import Port, PortLink
> -from .sut_node import SutNode
> -from .tg_node import TGNode
> -from .virtual_device import VirtualDevice
> diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
> index 1a77aee532..e5f5fcbe0e 100644
> --- a/dts/framework/testbed_model/os_session.py
> +++ b/dts/framework/testbed_model/os_session.py
> @@ -32,13 +32,13 @@
>  from framework.logger import DTSLogger
>  from framework.params import Params
>  from framework.remote_session import (
> -    CommandResult,
>      InteractiveRemoteSession,
> -    InteractiveShell,
>      RemoteSession,
>      create_interactive_session,
>      create_remote_session,
>  )
> +from framework.remote_session.interactive_shell import InteractiveShell
> +from framework.remote_session.remote_session import CommandResult
>  from framework.settings import SETTINGS
>  from framework.utils import MesonArgs
>
> diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
> index e1163106a3..83ad06ae2d 100644
> --- a/dts/framework/testbed_model/sut_node.py
> +++ b/dts/framework/testbed_model/sut_node.py
> @@ -26,7 +26,7 @@
>  )
>  from framework.params import Params, Switch
>  from framework.params.eal import EalParams
> -from framework.remote_session import CommandResult
> +from framework.remote_session.remote_session import CommandResult
>  from framework.settings import SETTINGS
>  from framework.utils import MesonArgs
>
> diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py
> index ed5467d825..7bc1c2cc08 100644
> --- a/dts/framework/testbed_model/traffic_generator/scapy.py
> +++ b/dts/framework/testbed_model/traffic_generator/scapy.py
> @@ -25,7 +25,7 @@
>  from scapy.packet import Packet  # type: ignore[import-untyped]
>
>  from framework.config import OS, ScapyTrafficGeneratorConfig
> -from framework.remote_session import PythonShell
> +from framework.remote_session.python_shell import PythonShell
>  from framework.settings import SETTINGS
>  from framework.testbed_model.node import Node
>  from framework.testbed_model.port import Port
> diff --git a/dts/tests/TestSuite_hello_world.py b/dts/tests/TestSuite_hello_world.py
> index fd7ff1534d..0d6995f260 100644
> --- a/dts/tests/TestSuite_hello_world.py
> +++ b/dts/tests/TestSuite_hello_world.py
> @@ -8,7 +8,7 @@
>  """
>
>  from framework.test_suite import TestSuite
> -from framework.testbed_model import (
> +from framework.testbed_model.cpu import (
>      LogicalCoreCount,
>      LogicalCoreCountFilter,
>      LogicalCoreList,
> diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py
> index a553e89662..ca678f662d 100644
> --- a/dts/tests/TestSuite_smoke_tests.py
> +++ b/dts/tests/TestSuite_smoke_tests.py
> @@ -15,7 +15,7 @@
>  import re
>
>  from framework.config import PortConfig
> -from framework.remote_session import TestPmdShell
> +from framework.remote_session.testpmd_shell import TestPmdShell
>  from framework.settings import SETTINGS
>  from framework.test_suite import TestSuite
>  from framework.utils import REGEX_FOR_PCI_ADDRESS
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v5 5/8] dts: add testpmd shell params
  2024-06-17 14:54   ` [PATCH v5 5/8] dts: add testpmd shell params Luca Vizzarro
@ 2024-06-17 15:24     ` Nicholas Pratte
  0 siblings, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-06-17 15:24 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Jeremy Spewock, Juraj Linkeš, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Mon, Jun 17, 2024 at 10:54 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Implement all the testpmd shell parameters into a data structure.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
> Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
> Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
> ---
>  dts/framework/params/testpmd.py               | 607 ++++++++++++++++++
>  dts/framework/remote_session/testpmd_shell.py |  39 +-
>  dts/tests/TestSuite_pmd_buffer_scatter.py     |   5 +-
>  3 files changed, 613 insertions(+), 38 deletions(-)
>  create mode 100644 dts/framework/params/testpmd.py
>
> diff --git a/dts/framework/params/testpmd.py b/dts/framework/params/testpmd.py
> new file mode 100644
> index 0000000000..1913bd0fa2
> --- /dev/null
> +++ b/dts/framework/params/testpmd.py
> @@ -0,0 +1,607 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2024 Arm Limited
> +
> +"""Module containing all the TestPmd-related parameter classes."""
> +
> +from dataclasses import dataclass, field
> +from enum import EnumMeta, Flag, auto, unique
> +from pathlib import PurePath
> +from typing import Literal, NamedTuple
> +
> +from framework.params import (
> +    Params,
> +    Switch,
> +    YesNoSwitch,
> +    bracketed,
> +    comma_separated,
> +    hex_from_flag_value,
> +    modify_str,
> +    str_from_flag_value,
> +)
> +from framework.params.eal import EalParams
> +from framework.utils import StrEnum
> +
> +
> +class PortTopology(StrEnum):
> +    """Enum representing the port topology."""
> +
> +    #: In paired mode, the forwarding is between pairs of ports, e.g.: (0,1), (2,3), (4,5).
> +    paired = auto()
> +
> +    #: In chained mode, the forwarding is to the next available port in the port mask, e.g.:
> +    #: (0,1), (1,2), (2,0).
> +    #:
> +    #: The ordering of the ports can be changed using the portlist testpmd runtime function.
> +    chained = auto()
> +
> +    #: In loop mode, ingress traffic is simply transmitted back on the same interface.
> +    loop = auto()
> +
> +
> +@modify_str(comma_separated, bracketed)
> +class PortNUMAConfig(NamedTuple):
> +    """DPDK port to NUMA socket association tuple."""
> +
> +    #:
> +    port: int
> +    #:
> +    socket: int
> +
> +
> +@modify_str(str_from_flag_value)
> +@unique
> +class FlowDirection(Flag):
> +    """Flag indicating the direction of the flow.
> +
> +    A bi-directional flow can be specified with the pipe:
> +
> +    >>> TestPmdFlowDirection.RX | TestPmdFlowDirection.TX
> +    <TestPmdFlowDirection.TX|RX: 3>
> +    """
> +
> +    #:
> +    RX = 1 << 0
> +    #:
> +    TX = 1 << 1
> +
> +
> +@modify_str(comma_separated, bracketed)
> +class RingNUMAConfig(NamedTuple):
> +    """Tuple associating DPDK port, direction of the flow and NUMA socket."""
> +
> +    #:
> +    port: int
> +    #:
> +    direction: FlowDirection
> +    #:
> +    socket: int
> +
> +
> +@modify_str(comma_separated)
> +class EthPeer(NamedTuple):
> +    """Tuple associating a MAC address to the specified DPDK port."""
> +
> +    #:
> +    port_no: int
> +    #:
> +    mac_address: str
> +
> +
> +@modify_str(comma_separated)
> +class TxIPAddrPair(NamedTuple):
> +    """Tuple specifying the source and destination IPs for the packets."""
> +
> +    #:
> +    source_ip: str
> +    #:
> +    dest_ip: str
> +
> +
> +@modify_str(comma_separated)
> +class TxUDPPortPair(NamedTuple):
> +    """Tuple specifying the UDP source and destination ports for the packets.
> +
> +    If leaving ``dest_port`` unspecified, ``source_port`` will be used for
> +    the destination port as well.
> +    """
> +
> +    #:
> +    source_port: int
> +    #:
> +    dest_port: int | None = None
> +
> +
> +@dataclass
> +class DisableRSS(Params):
> +    """Disables RSS (Receive Side Scaling)."""
> +
> +    _disable_rss: Literal[True] = field(
> +        default=True, init=False, metadata=Params.long("disable-rss")
> +    )
> +
> +
> +@dataclass
> +class SetRSSIPOnly(Params):
> +    """Sets RSS (Receive Side Scaling) functions for IPv4/IPv6 only."""
> +
> +    _rss_ip: Literal[True] = field(default=True, init=False, metadata=Params.long("rss-ip"))
> +
> +
> +@dataclass
> +class SetRSSUDP(Params):
> +    """Sets RSS (Receive Side Scaling) functions for IPv4/IPv6 and UDP."""
> +
> +    _rss_udp: Literal[True] = field(default=True, init=False, metadata=Params.long("rss-udp"))
> +
> +
> +class RSSSetting(EnumMeta):
> +    """Enum representing a RSS setting. Each property is a class that needs to be initialised."""
> +
> +    #:
> +    Disabled = DisableRSS
> +    #:
> +    SetIPOnly = SetRSSIPOnly
> +    #:
> +    SetUDP = SetRSSUDP
> +
> +
> +class SimpleForwardingModes(StrEnum):
> +    r"""The supported packet forwarding modes for :class:`~TestPmdShell`\s."""
> +
> +    #:
> +    io = auto()
> +    #:
> +    mac = auto()
> +    #:
> +    macswap = auto()
> +    #:
> +    rxonly = auto()
> +    #:
> +    csum = auto()
> +    #:
> +    icmpecho = auto()
> +    #:
> +    ieee1588 = auto()
> +    #:
> +    fivetswap = "5tswap"
> +    #:
> +    shared_rxq = "shared-rxq"
> +    #:
> +    recycle_mbufs = auto()
> +
> +
> +@dataclass(kw_only=True)
> +class TXOnlyForwardingMode(Params):
> +    """Sets a TX-Only forwarding mode.
> +
> +    Attributes:
> +        multi_flow: Generates multiple flows if set to True.
> +        segments_length: Sets TX segment sizes or total packet length.
> +    """
> +
> +    _forward_mode: Literal["txonly"] = field(
> +        default="txonly", init=False, metadata=Params.long("forward-mode")
> +    )
> +    multi_flow: Switch = field(default=None, metadata=Params.long("txonly-multi-flow"))
> +    segments_length: list[int] | None = field(
> +        default=None, metadata=Params.long("txpkts") | Params.convert_value(comma_separated)
> +    )
> +
> +
> +@dataclass(kw_only=True)
> +class FlowGenForwardingMode(Params):
> +    """Sets a flowgen forwarding mode.
> +
> +    Attributes:
> +        clones: Set the number of each packet clones to be sent. Sending clones reduces host CPU
> +                load on creating packets and may help in testing extreme speeds or maxing out
> +                Tx packet performance. N should be not zero, but less than ‘burst’ parameter.
> +        flows: Set the number of flows to be generated, where 1 <= N <= INT32_MAX.
> +        segments_length: Set TX segment sizes or total packet length.
> +    """
> +
> +    _forward_mode: Literal["flowgen"] = field(
> +        default="flowgen", init=False, metadata=Params.long("forward-mode")
> +    )
> +    clones: int | None = field(default=None, metadata=Params.long("flowgen-clones"))
> +    flows: int | None = field(default=None, metadata=Params.long("flowgen-flows"))
> +    segments_length: list[int] | None = field(
> +        default=None, metadata=Params.long("txpkts") | Params.convert_value(comma_separated)
> +    )
> +
> +
> +@dataclass(kw_only=True)
> +class NoisyForwardingMode(Params):
> +    """Sets a noisy forwarding mode.
> +
> +    Attributes:
> +        forward_mode: Set the noisy VNF forwarding mode.
> +        tx_sw_buffer_size: Set the maximum number of elements of the FIFO queue to be created for
> +                           buffering packets.
> +        tx_sw_buffer_flushtime: Set the time before packets in the FIFO queue are flushed.
> +        lkup_memory: Set the size of the noisy neighbor simulation memory buffer in MB to N.
> +        lkup_num_reads: Set the size of the noisy neighbor simulation memory buffer in MB to N.
> +        lkup_num_writes: Set the number of writes to be done in noisy neighbor simulation
> +                         memory buffer to N.
> +        lkup_num_reads_writes: Set the number of r/w accesses to be done in noisy neighbor
> +                               simulation memory buffer to N.
> +    """
> +
> +    _forward_mode: Literal["noisy"] = field(
> +        default="noisy", init=False, metadata=Params.long("forward-mode")
> +    )
> +    forward_mode: (
> +        Literal[
> +            SimpleForwardingModes.io,
> +            SimpleForwardingModes.mac,
> +            SimpleForwardingModes.macswap,
> +            SimpleForwardingModes.fivetswap,
> +        ]
> +        | None
> +    ) = field(default=SimpleForwardingModes.io, metadata=Params.long("noisy-forward-mode"))
> +    tx_sw_buffer_size: int | None = field(
> +        default=None, metadata=Params.long("noisy-tx-sw-buffer-size")
> +    )
> +    tx_sw_buffer_flushtime: int | None = field(
> +        default=None, metadata=Params.long("noisy-tx-sw-buffer-flushtime")
> +    )
> +    lkup_memory: int | None = field(default=None, metadata=Params.long("noisy-lkup-memory"))
> +    lkup_num_reads: int | None = field(default=None, metadata=Params.long("noisy-lkup-num-reads"))
> +    lkup_num_writes: int | None = field(default=None, metadata=Params.long("noisy-lkup-num-writes"))
> +    lkup_num_reads_writes: int | None = field(
> +        default=None, metadata=Params.long("noisy-lkup-num-reads-writes")
> +    )
> +
> +
> +@modify_str(hex_from_flag_value)
> +@unique
> +class HairpinMode(Flag):
> +    """Flag representing the hairpin mode."""
> +
> +    #: Two hairpin ports loop.
> +    TWO_PORTS_LOOP = 1 << 0
> +    #: Two hairpin ports paired.
> +    TWO_PORTS_PAIRED = 1 << 1
> +    #: Explicit Tx flow rule.
> +    EXPLICIT_TX_FLOW = 1 << 4
> +    #: Force memory settings of hairpin RX queue.
> +    FORCE_RX_QUEUE_MEM_SETTINGS = 1 << 8
> +    #: Force memory settings of hairpin TX queue.
> +    FORCE_TX_QUEUE_MEM_SETTINGS = 1 << 9
> +    #: Hairpin RX queues will use locked device memory.
> +    RX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 12
> +    #: Hairpin RX queues will use RTE memory.
> +    RX_QUEUE_USE_RTE_MEMORY = 1 << 13
> +    #: Hairpin TX queues will use locked device memory.
> +    TX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 16
> +    #: Hairpin TX queues will use RTE memory.
> +    TX_QUEUE_USE_RTE_MEMORY = 1 << 18
> +
> +
> +@dataclass(kw_only=True)
> +class RXRingParams(Params):
> +    """Sets the RX ring parameters.
> +
> +    Attributes:
> +        descriptors: Set the number of descriptors in the RX rings to N, where N > 0.
> +        prefetch_threshold: Set the prefetch threshold register of RX rings to N, where N >= 0.
> +        host_threshold: Set the host threshold register of RX rings to N, where N >= 0.
> +        write_back_threshold: Set the write-back threshold register of RX rings to N, where N >= 0.
> +        free_threshold: Set the free threshold of RX descriptors to N,
> +                        where 0 <= N < value of ``-–rxd``.
> +    """
> +
> +    descriptors: int | None = field(default=None, metadata=Params.long("rxd"))
> +    prefetch_threshold: int | None = field(default=None, metadata=Params.long("rxpt"))
> +    host_threshold: int | None = field(default=None, metadata=Params.long("rxht"))
> +    write_back_threshold: int | None = field(default=None, metadata=Params.long("rxwt"))
> +    free_threshold: int | None = field(default=None, metadata=Params.long("rxfreet"))
> +
> +
> +@modify_str(hex_from_flag_value)
> +@unique
> +class RXMultiQueueMode(Flag):
> +    """Flag representing the RX multi-queue mode."""
> +
> +    #:
> +    RSS = 1 << 0
> +    #:
> +    DCB = 1 << 1
> +    #:
> +    VMDQ = 1 << 2
> +
> +
> +@dataclass(kw_only=True)
> +class TXRingParams(Params):
> +    """Sets the TX ring parameters.
> +
> +    Attributes:
> +        descriptors: Set the number of descriptors in the TX rings to N, where N > 0.
> +        rs_bit_threshold: Set the transmit RS bit threshold of TX rings to N,
> +                          where 0 <= N <= value of ``--txd``.
> +        prefetch_threshold: Set the prefetch threshold register of TX rings to N, where N >= 0.
> +        host_threshold: Set the host threshold register of TX rings to N, where N >= 0.
> +        write_back_threshold: Set the write-back threshold register of TX rings to N, where N >= 0.
> +        free_threshold: Set the transmit free threshold of TX rings to N,
> +                        where 0 <= N <= value of ``--txd``.
> +    """
> +
> +    descriptors: int | None = field(default=None, metadata=Params.long("txd"))
> +    rs_bit_threshold: int | None = field(default=None, metadata=Params.long("txrst"))
> +    prefetch_threshold: int | None = field(default=None, metadata=Params.long("txpt"))
> +    host_threshold: int | None = field(default=None, metadata=Params.long("txht"))
> +    write_back_threshold: int | None = field(default=None, metadata=Params.long("txwt"))
> +    free_threshold: int | None = field(default=None, metadata=Params.long("txfreet"))
> +
> +
> +class Event(StrEnum):
> +    """Enum representing a testpmd event."""
> +
> +    #:
> +    unknown = auto()
> +    #:
> +    queue_state = auto()
> +    #:
> +    vf_mbox = auto()
> +    #:
> +    macsec = auto()
> +    #:
> +    intr_lsc = auto()
> +    #:
> +    intr_rmv = auto()
> +    #:
> +    intr_reset = auto()
> +    #:
> +    dev_probed = auto()
> +    #:
> +    dev_released = auto()
> +    #:
> +    flow_aged = auto()
> +    #:
> +    err_recovering = auto()
> +    #:
> +    recovery_success = auto()
> +    #:
> +    recovery_failed = auto()
> +    #:
> +    all = auto()
> +
> +
> +class SimpleMempoolAllocationMode(StrEnum):
> +    """Enum representing simple mempool allocation modes."""
> +
> +    #: Create and populate mempool using native DPDK memory.
> +    native = auto()
> +    #: Create and populate mempool using externally and anonymously allocated area.
> +    xmem = auto()
> +    #: Create and populate mempool using externally and anonymously allocated hugepage area.
> +    xmemhuge = auto()
> +
> +
> +@dataclass(kw_only=True)
> +class AnonMempoolAllocationMode(Params):
> +    """Create mempool using native DPDK memory, but populate using anonymous memory.
> +
> +    Attributes:
> +        no_iova_contig: Enables to create mempool which is not IOVA contiguous.
> +    """
> +
> +    _mp_alloc: Literal["anon"] = field(default="anon", init=False, metadata=Params.long("mp-alloc"))
> +    no_iova_contig: Switch = None
> +
> +
> +@dataclass(slots=True, kw_only=True)
> +class TestPmdParams(EalParams):
> +    """The testpmd shell parameters.
> +
> +    Attributes:
> +        interactive_mode: Run testpmd in interactive mode.
> +        auto_start: Start forwarding on initialization.
> +        tx_first: Start forwarding, after sending a burst of packets first.
> +        stats_period: Display statistics every ``PERIOD`` seconds, if interactive mode is disabled.
> +                      The default value is 0, which means that the statistics will not be displayed.
> +
> +                      .. note:: This flag should be used only in non-interactive mode.
> +        display_xstats: Display comma-separated list of extended statistics every ``PERIOD`` seconds
> +                        as specified in ``--stats-period`` or when used with interactive commands
> +                        that show Rx/Tx statistics (i.e. ‘show port stats’).
> +        nb_cores: Set the number of forwarding cores, where 1 <= N <= “number of cores” or
> +                  ``RTE_MAX_LCORE`` from the configuration file.
> +        coremask: Set the bitmask of the cores running the packet forwarding test. The main
> +                  lcore is reserved for command line parsing only and cannot be masked on for packet
> +                  forwarding.
> +        nb_ports: Set the number of forwarding ports, where 1 <= N <= “number of ports” on the board
> +                  or ``RTE_MAX_ETHPORTS`` from the configuration file. The default value is the
> +                  number of ports on the board.
> +        port_topology: Set port topology, where mode is paired (the default), chained or loop.
> +        portmask: Set the bitmask of the ports used by the packet forwarding test.
> +        portlist: Set the forwarding ports based on the user input used by the packet forwarding
> +                  test. ‘-‘ denotes a range of ports to set including the two specified port IDs ‘,’
> +                  separates multiple port values. Possible examples like –portlist=0,1 or
> +                  –portlist=0-2 or –portlist=0,1-2 etc.
> +        numa: Enable/disable NUMA-aware allocation of RX/TX rings and of RX memory buffers (mbufs).
> +        socket_num: Set the socket from which all memory is allocated in NUMA mode, where
> +                    0 <= N < number of sockets on the board.
> +        port_numa_config: Specify the socket on which the memory pool to be used by the port will be
> +                          allocated.
> +        ring_numa_config: Specify the socket on which the TX/RX rings for the port will be
> +                          allocated. Where flag is 1 for RX, 2 for TX, and 3 for RX and TX.
> +        total_num_mbufs: Set the number of mbufs to be allocated in the mbuf pools, where N > 1024.
> +        mbuf_size: Set the data size of the mbufs used to N bytes, where N < 65536.
> +                   If multiple mbuf-size values are specified the extra memory pools will be created
> +                   for allocating mbufs to receive packets with buffer splitting features.
> +        mbcache: Set the cache of mbuf memory pools to N, where 0 <= N <= 512.
> +        max_pkt_len: Set the maximum packet size to N bytes, where N >= 64.
> +        eth_peers_configfile: Use a configuration file containing the Ethernet addresses of
> +                              the peer ports.
> +        eth_peer: Set the MAC address XX:XX:XX:XX:XX:XX of the peer port N,
> +                  where 0 <= N < RTE_MAX_ETHPORTS.
> +        tx_ip: Set the source and destination IP address used when doing transmit only test.
> +               The defaults address values are source 198.18.0.1 and destination 198.18.0.2.
> +               These are special purpose addresses reserved for benchmarking (RFC 5735).
> +        tx_udp: Set the source and destination UDP port number for transmit test only test.
> +                The default port is the port 9 which is defined for the discard protocol (RFC 863).
> +        enable_lro: Enable large receive offload.
> +        max_lro_pkt_size: Set the maximum LRO aggregated packet size to N bytes, where N >= 64.
> +        disable_crc_strip: Disable hardware CRC stripping.
> +        enable_scatter: Enable scatter (multi-segment) RX.
> +        enable_hw_vlan: Enable hardware VLAN.
> +        enable_hw_vlan_filter: Enable hardware VLAN filter.
> +        enable_hw_vlan_strip: Enable hardware VLAN strip.
> +        enable_hw_vlan_extend: Enable hardware VLAN extend.
> +        enable_hw_qinq_strip: Enable hardware QINQ strip.
> +        pkt_drop_enabled: Enable per-queue packet drop for packets with no descriptors.
> +        rss: Receive Side Scaling setting.
> +        forward_mode: Set the forwarding mode.
> +        hairpin_mode: Set the hairpin port configuration.
> +        hairpin_queues: Set the number of hairpin queues per port to N, where 1 <= N <= 65535.
> +        burst: Set the number of packets per burst to N, where 1 <= N <= 512.
> +        enable_rx_cksum: Enable hardware RX checksum offload.
> +        rx_queues: Set the number of RX queues per port to N, where 1 <= N <= 65535.
> +        rx_ring: Set the RX rings parameters.
> +        no_flush_rx: Don’t flush the RX streams before starting forwarding. Used mainly with
> +                     the PCAP PMD.
> +        rx_segments_offsets: Set the offsets of packet segments on receiving
> +                             if split feature is engaged.
> +        rx_segments_length: Set the length of segments to scatter packets on receiving
> +                            if split feature is engaged.
> +        multi_rx_mempool: Enable multiple mbuf pools per Rx queue.
> +        rx_shared_queue: Create queues in shared Rx queue mode if device supports. Shared Rx queues
> +                         are grouped per X ports. X defaults to UINT32_MAX, implies all ports join
> +                         share group 1. Forwarding engine “shared-rxq” should be used for shared Rx
> +                         queues. This engine does Rx only and update stream statistics accordingly.
> +        rx_offloads: Set the bitmask of RX queue offloads.
> +        rx_mq_mode: Set the RX multi queue mode which can be enabled.
> +        tx_queues: Set the number of TX queues per port to N, where 1 <= N <= 65535.
> +        tx_ring: Set the TX rings params.
> +        tx_offloads: Set the hexadecimal bitmask of TX queue offloads.
> +        eth_link_speed: Set a forced link speed to the ethernet port. E.g. 1000 for 1Gbps.
> +        disable_link_check: Disable check on link status when starting/stopping ports.
> +        disable_device_start: Do not automatically start all ports. This allows testing
> +                              configuration of rx and tx queues before device is started
> +                              for the first time.
> +        no_lsc_interrupt: Disable LSC interrupts for all ports, even those supporting it.
> +        no_rmv_interrupt: Disable RMV interrupts for all ports, even those supporting it.
> +        bitrate_stats: Set the logical core N to perform bitrate calculation.
> +        latencystats: Set the logical core N to perform latency and jitter calculations.
> +        print_events: Enable printing the occurrence of the designated events.
> +                      Using :attr:`TestPmdEvent.ALL` will enable all of them.
> +        mask_events: Disable printing the occurrence of the designated events.
> +                     Using :attr:`TestPmdEvent.ALL` will disable all of them.
> +        flow_isolate_all: Providing this parameter requests flow API isolated mode on all ports at
> +                          initialization time. It ensures all traffic is received through the
> +                          configured flow rules only (see flow command). Ports that do not support
> +                          this mode are automatically discarded.
> +        disable_flow_flush: Disable port flow flush when stopping port.
> +                            This allows testing keep flow rules or shared flow objects across
> +                            restart.
> +        hot_plug: Enable device event monitor mechanism for hotplug.
> +        vxlan_gpe_port: Set the UDP port number of tunnel VXLAN-GPE to N.
> +        geneve_parsed_port: Set the UDP port number that is used for parsing the GENEVE protocol
> +                            to N. HW may be configured with another tunnel Geneve port.
> +        lock_all_memory: Enable/disable locking all memory. Disabled by default.
> +        mempool_allocation_mode: Set mempool allocation mode.
> +        record_core_cycles: Enable measurement of CPU cycles per packet.
> +        record_burst_status: Enable display of RX and TX burst stats.
> +    """
> +
> +    interactive_mode: Switch = field(default=True, metadata=Params.short("i"))
> +    auto_start: Switch = field(default=None, metadata=Params.short("a"))
> +    tx_first: Switch = None
> +    stats_period: int | None = None
> +    display_xstats: list[str] | None = field(
> +        default=None, metadata=Params.convert_value(comma_separated)
> +    )
> +    nb_cores: int | None = None
> +    coremask: int | None = field(default=None, metadata=Params.convert_value(hex))
> +    nb_ports: int | None = None
> +    port_topology: PortTopology | None = PortTopology.paired
> +    portmask: int | None = field(default=None, metadata=Params.convert_value(hex))
> +    portlist: str | None = None  # TODO: can be ranges 0,1-3
> +
> +    numa: YesNoSwitch = None
> +    socket_num: int | None = None
> +    port_numa_config: list[PortNUMAConfig] | None = field(
> +        default=None, metadata=Params.convert_value(comma_separated)
> +    )
> +    ring_numa_config: list[RingNUMAConfig] | None = field(
> +        default=None, metadata=Params.convert_value(comma_separated)
> +    )
> +    total_num_mbufs: int | None = None
> +    mbuf_size: list[int] | None = field(
> +        default=None, metadata=Params.convert_value(comma_separated)
> +    )
> +    mbcache: int | None = None
> +    max_pkt_len: int | None = None
> +    eth_peers_configfile: PurePath | None = None
> +    eth_peer: list[EthPeer] | None = field(default=None, metadata=Params.multiple())
> +    tx_ip: TxIPAddrPair | None = None
> +    tx_udp: TxUDPPortPair | None = None
> +    enable_lro: Switch = None
> +    max_lro_pkt_size: int | None = None
> +    disable_crc_strip: Switch = None
> +    enable_scatter: Switch = None
> +    enable_hw_vlan: Switch = None
> +    enable_hw_vlan_filter: Switch = None
> +    enable_hw_vlan_strip: Switch = None
> +    enable_hw_vlan_extend: Switch = None
> +    enable_hw_qinq_strip: Switch = None
> +    pkt_drop_enabled: Switch = field(default=None, metadata=Params.long("enable-drop-en"))
> +    rss: RSSSetting | None = None
> +    forward_mode: (
> +        SimpleForwardingModes
> +        | FlowGenForwardingMode
> +        | TXOnlyForwardingMode
> +        | NoisyForwardingMode
> +        | None
> +    ) = None
> +    hairpin_mode: HairpinMode | None = None
> +    hairpin_queues: int | None = field(default=None, metadata=Params.long("hairpinq"))
> +    burst: int | None = None
> +    enable_rx_cksum: Switch = None
> +
> +    rx_queues: int | None = field(default=None, metadata=Params.long("rxq"))
> +    rx_ring: RXRingParams | None = None
> +    no_flush_rx: Switch = None
> +    rx_segments_offsets: list[int] | None = field(
> +        default=None, metadata=Params.long("rxoffs") | Params.convert_value(comma_separated)
> +    )
> +    rx_segments_length: list[int] | None = field(
> +        default=None, metadata=Params.long("rxpkts") | Params.convert_value(comma_separated)
> +    )
> +    multi_rx_mempool: Switch = None
> +    rx_shared_queue: Switch | int = field(default=None, metadata=Params.long("rxq-share"))
> +    rx_offloads: int | None = field(default=None, metadata=Params.convert_value(hex))
> +    rx_mq_mode: RXMultiQueueMode | None = None
> +
> +    tx_queues: int | None = field(default=None, metadata=Params.long("txq"))
> +    tx_ring: TXRingParams | None = None
> +    tx_offloads: int | None = field(default=None, metadata=Params.convert_value(hex))
> +
> +    eth_link_speed: int | None = None
> +    disable_link_check: Switch = None
> +    disable_device_start: Switch = None
> +    no_lsc_interrupt: Switch = None
> +    no_rmv_interrupt: Switch = None
> +    bitrate_stats: int | None = None
> +    latencystats: int | None = None
> +    print_events: list[Event] | None = field(
> +        default=None, metadata=Params.multiple() | Params.long("print-event")
> +    )
> +    mask_events: list[Event] | None = field(
> +        default_factory=lambda: [Event.intr_lsc],
> +        metadata=Params.multiple() | Params.long("mask-event"),
> +    )
> +
> +    flow_isolate_all: Switch = None
> +    disable_flow_flush: Switch = None
> +
> +    hot_plug: Switch = None
> +    vxlan_gpe_port: int | None = None
> +    geneve_parsed_port: int | None = None
> +    lock_all_memory: YesNoSwitch = field(default=None, metadata=Params.long("mlockall"))
> +    mempool_allocation_mode: SimpleMempoolAllocationMode | AnonMempoolAllocationMode | None = field(
> +        default=None, metadata=Params.long("mp-alloc")
> +    )
> +    record_core_cycles: Switch = None
> +    record_burst_status: Switch = None
> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
> index 2b9ef9418d..82701a9839 100644
> --- a/dts/framework/remote_session/testpmd_shell.py
> +++ b/dts/framework/remote_session/testpmd_shell.py
> @@ -26,7 +26,7 @@
>  from typing_extensions import Self
>
>  from framework.exception import InteractiveCommandExecutionError
> -from framework.params.eal import EalParams
> +from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
>  from framework.parser import ParserFn, TextParser
>  from framework.settings import SETTINGS
>  from framework.utils import StrEnum
> @@ -56,37 +56,6 @@ def __str__(self) -> str:
>          return self.pci_address
>
>
> -class TestPmdForwardingModes(StrEnum):
> -    r"""The supported packet forwarding modes for :class:`~TestPmdShell`\s."""
> -
> -    #:
> -    io = auto()
> -    #:
> -    mac = auto()
> -    #:
> -    macswap = auto()
> -    #:
> -    flowgen = auto()
> -    #:
> -    rxonly = auto()
> -    #:
> -    txonly = auto()
> -    #:
> -    csum = auto()
> -    #:
> -    icmpecho = auto()
> -    #:
> -    ieee1588 = auto()
> -    #:
> -    noisy = auto()
> -    #:
> -    fivetswap = "5tswap"
> -    #:
> -    shared_rxq = "shared-rxq"
> -    #:
> -    recycle_mbufs = auto()
> -
> -
>  class VLANOffloadFlag(Flag):
>      """Flag representing the VLAN offload settings of a NIC port."""
>
> @@ -646,9 +615,7 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
>          Also find the number of pci addresses which were allowed on the command line when the app
>          was started.
>          """
> -        self._app_params += " -i --mask-event intr_lsc"
> -
> -        assert isinstance(self._app_params, EalParams)
> +        assert isinstance(self._app_params, TestPmdParams)
>
>          self.number_of_ports = (
>              len(self._app_params.ports) if self._app_params.ports is not None else 0
> @@ -740,7 +707,7 @@ def wait_link_status_up(self, port_id: int, timeout=SETTINGS.timeout) -> bool:
>              self._logger.error(f"The link for port {port_id} did not come up in the given timeout.")
>          return "Link status: up" in port_info
>
> -    def set_forward_mode(self, mode: TestPmdForwardingModes, verify: bool = True):
> +    def set_forward_mode(self, mode: SimpleForwardingModes, verify: bool = True):
>          """Set packet forwarding mode.
>
>          Args:
> diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
> index c6e93839cb..578b5a4318 100644
> --- a/dts/tests/TestSuite_pmd_buffer_scatter.py
> +++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
> @@ -23,7 +23,8 @@
>  from scapy.utils import hexstr  # type: ignore[import-untyped]
>
>  from framework.params import Params
> -from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
> +from framework.params.testpmd import SimpleForwardingModes
> +from framework.remote_session.testpmd_shell import TestPmdShell
>  from framework.test_suite import TestSuite
>
>
> @@ -113,7 +114,7 @@ def pmd_scatter(self, mbsize: int) -> None:
>              ),
>              privileged=True,
>          )
> -        testpmd.set_forward_mode(TestPmdForwardingModes.mac)
> +        testpmd.set_forward_mode(SimpleForwardingModes.mac)
>          testpmd.start()
>
>          for offset in [-1, 0, 1, 4, 5]:
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v5 6/8] dts: use testpmd params for scatter test suite
  2024-06-17 14:54   ` [PATCH v5 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
@ 2024-06-17 15:24     ` Nicholas Pratte
  0 siblings, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-06-17 15:24 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Jeremy Spewock, Juraj Linkeš, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Mon, Jun 17, 2024 at 10:54 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Update the buffer scatter test suite to use TestPmdParameters
> instead of the StrParams implementation.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
> Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
> Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
> ---
>  dts/tests/TestSuite_pmd_buffer_scatter.py | 18 +++++++++---------
>  1 file changed, 9 insertions(+), 9 deletions(-)
>
> diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
> index 578b5a4318..6d206c1a40 100644
> --- a/dts/tests/TestSuite_pmd_buffer_scatter.py
> +++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
> @@ -16,14 +16,14 @@
>  """
>
>  import struct
> +from dataclasses import asdict
>
>  from scapy.layers.inet import IP  # type: ignore[import-untyped]
>  from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
>  from scapy.packet import Raw  # type: ignore[import-untyped]
>  from scapy.utils import hexstr  # type: ignore[import-untyped]
>
> -from framework.params import Params
> -from framework.params.testpmd import SimpleForwardingModes
> +from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
>  from framework.remote_session.testpmd_shell import TestPmdShell
>  from framework.test_suite import TestSuite
>
> @@ -105,16 +105,16 @@ def pmd_scatter(self, mbsize: int) -> None:
>          """
>          testpmd = self.sut_node.create_interactive_shell(
>              TestPmdShell,
> -            app_params=Params.from_str(
> -                "--mbcache=200 "
> -                f"--mbuf-size={mbsize} "
> -                "--max-pkt-len=9000 "
> -                "--port-topology=paired "
> -                "--tx-offloads=0x00008000"
> +            app_params=TestPmdParams(
> +                forward_mode=SimpleForwardingModes.mac,
> +                mbcache=200,
> +                mbuf_size=[mbsize],
> +                max_pkt_len=9000,
> +                tx_offloads=0x00008000,
> +                **asdict(self.sut_node.create_eal_parameters()),
>              ),
>              privileged=True,
>          )
> -        testpmd.set_forward_mode(SimpleForwardingModes.mac)
>          testpmd.start()
>
>          for offset in [-1, 0, 1, 4, 5]:
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v5 7/8] dts: rework interactive shells
  2024-06-17 14:54   ` [PATCH v5 7/8] dts: rework interactive shells Luca Vizzarro
@ 2024-06-17 15:25     ` Nicholas Pratte
  0 siblings, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-06-17 15:25 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Jeremy Spewock, Juraj Linkeš, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Mon, Jun 17, 2024 at 10:54 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> The way nodes and interactive shells interact makes it difficult to
> develop for static type checking and hinting. The current system relies
> on a top-down approach, attempting to give a generic interface to the
> test developer, hiding the interaction of concrete shell classes as much
> as possible. When working with strong typing this approach is not ideal,
> as Python's implementation of generics is still rudimentary.
>
> This rework reverses the tests interaction to a bottom-up approach,
> allowing the test developer to call concrete shell classes directly,
> and let them ingest nodes independently. While also re-enforcing type
> checking and making the code easier to read.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> ---
>  dts/framework/params/eal.py                   |   6 +-
>  dts/framework/remote_session/dpdk_shell.py    | 106 +++++++++++++++
>  .../remote_session/interactive_shell.py       |  79 ++++++-----
>  dts/framework/remote_session/python_shell.py  |   4 +-
>  dts/framework/remote_session/testpmd_shell.py |  64 +++++----
>  dts/framework/testbed_model/node.py           |  36 +----
>  dts/framework/testbed_model/os_session.py     |  36 +----
>  dts/framework/testbed_model/sut_node.py       | 124 +-----------------
>  .../testbed_model/traffic_generator/scapy.py  |   4 +-
>  dts/tests/TestSuite_hello_world.py            |   7 +-
>  dts/tests/TestSuite_pmd_buffer_scatter.py     |  21 ++-
>  dts/tests/TestSuite_smoke_tests.py            |   2 +-
>  12 files changed, 210 insertions(+), 279 deletions(-)
>  create mode 100644 dts/framework/remote_session/dpdk_shell.py
>
> diff --git a/dts/framework/params/eal.py b/dts/framework/params/eal.py
> index bbdbc8f334..8d7766fefc 100644
> --- a/dts/framework/params/eal.py
> +++ b/dts/framework/params/eal.py
> @@ -35,9 +35,9 @@ class EalParams(Params):
>                  ``other_eal_param='--single-file-segments'``
>      """
>
> -    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
> -    memory_channels: int = field(metadata=Params.short("n"))
> -    prefix: str = field(metadata=Params.long("file-prefix"))
> +    lcore_list: LogicalCoreList | None = field(default=None, metadata=Params.short("l"))
> +    memory_channels: int | None = field(default=None, metadata=Params.short("n"))
> +    prefix: str = field(default="dpdk", metadata=Params.long("file-prefix"))
>      no_pci: Switch = None
>      vdevs: list[VirtualDevice] | None = field(
>          default=None, metadata=Params.multiple() | Params.long("vdev")
> diff --git a/dts/framework/remote_session/dpdk_shell.py b/dts/framework/remote_session/dpdk_shell.py
> new file mode 100644
> index 0000000000..2cbf69ae9a
> --- /dev/null
> +++ b/dts/framework/remote_session/dpdk_shell.py
> @@ -0,0 +1,106 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2024 Arm Limited
> +
> +"""Base interactive shell for DPDK applications.
> +
> +Provides a base class to create interactive shells based on DPDK.
> +"""
> +
> +
> +from abc import ABC
> +
> +from framework.params.eal import EalParams
> +from framework.remote_session.interactive_shell import InteractiveShell
> +from framework.settings import SETTINGS
> +from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
> +from framework.testbed_model.sut_node import SutNode
> +
> +
> +def compute_eal_params(
> +    sut_node: SutNode,
> +    params: EalParams | None = None,
> +    lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
> +    ascending_cores: bool = True,
> +    append_prefix_timestamp: bool = True,
> +) -> EalParams:
> +    """Compute EAL parameters based on the node's specifications.
> +
> +    Args:
> +        sut_node: The SUT node to compute the values for.
> +        params: If set to None a new object is created and returned. Otherwise the given
> +            :class:`EalParams`'s lcore_list is modified according to the given filter specifier.
> +            A DPDK prefix is added. If ports is set to None, all the SUT node's ports are
> +            automatically assigned.
> +        lcore_filter_specifier: A number of lcores/cores/sockets to use or a list of lcore ids to
> +            use. The default will select one lcore for each of two cores on one socket, in ascending
> +            order of core ids.
> +        ascending_cores: Sort cores in ascending order (lowest to highest IDs). If :data:`False`,
> +            sort in descending order.
> +        append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.
> +    """
> +    if params is None:
> +        params = EalParams()
> +
> +    if params.lcore_list is None:
> +        params.lcore_list = LogicalCoreList(
> +            sut_node.filter_lcores(lcore_filter_specifier, ascending_cores)
> +        )
> +
> +    prefix = params.prefix
> +    if append_prefix_timestamp:
> +        prefix = f"{prefix}_{sut_node.dpdk_timestamp}"
> +    prefix = sut_node.main_session.get_dpdk_file_prefix(prefix)
> +    if prefix:
> +        sut_node.dpdk_prefix_list.append(prefix)
> +    params.prefix = prefix
> +
> +    if params.ports is None:
> +        params.ports = sut_node.ports
> +
> +    return params
> +
> +
> +class DPDKShell(InteractiveShell, ABC):
> +    """The base class for managing DPDK-based interactive shells.
> +
> +    This class shouldn't be instantiated directly, but instead be extended.
> +    It automatically injects computed EAL parameters based on the node in the
> +    supplied app parameters.
> +    """
> +
> +    _node: SutNode
> +    _app_params: EalParams
> +
> +    _lcore_filter_specifier: LogicalCoreCount | LogicalCoreList
> +    _ascending_cores: bool
> +    _append_prefix_timestamp: bool
> +
> +    def __init__(
> +        self,
> +        node: SutNode,
> +        privileged: bool = True,
> +        timeout: float = SETTINGS.timeout,
> +        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
> +        ascending_cores: bool = True,
> +        append_prefix_timestamp: bool = True,
> +        start_on_init: bool = True,
> +        app_params: EalParams = EalParams(),
> +    ) -> None:
> +        """Overrides :meth:`~.interactive_shell.InteractiveShell.__init__`."""
> +        self._lcore_filter_specifier = lcore_filter_specifier
> +        self._ascending_cores = ascending_cores
> +        self._append_prefix_timestamp = append_prefix_timestamp
> +
> +        super().__init__(node, privileged, timeout, start_on_init, app_params)
> +
> +    def _post_init(self):
> +        """Computes EAL params based on the node capabilities before start."""
> +        self._app_params = compute_eal_params(
> +            self._node,
> +            self._app_params,
> +            self._lcore_filter_specifier,
> +            self._ascending_cores,
> +            self._append_prefix_timestamp,
> +        )
> +
> +        self._update_path(self._node.remote_dpdk_build_dir.joinpath(self.path))
> diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
> index 8191b36630..5a8a6d6d15 100644
> --- a/dts/framework/remote_session/interactive_shell.py
> +++ b/dts/framework/remote_session/interactive_shell.py
> @@ -17,13 +17,14 @@
>
>  from abc import ABC
>  from pathlib import PurePath
> -from typing import Callable, ClassVar
> +from typing import ClassVar
>
> -from paramiko import Channel, SSHClient, channel  # type: ignore[import-untyped]
> +from paramiko import Channel, channel  # type: ignore[import-untyped]
>
>  from framework.logger import DTSLogger
>  from framework.params import Params
>  from framework.settings import SETTINGS
> +from framework.testbed_model.node import Node
>
>
>  class InteractiveShell(ABC):
> @@ -36,13 +37,14 @@ class InteractiveShell(ABC):
>      session.
>      """
>
> -    _interactive_session: SSHClient
> +    _node: Node
>      _stdin: channel.ChannelStdinFile
>      _stdout: channel.ChannelFile
>      _ssh_channel: Channel
>      _logger: DTSLogger
>      _timeout: float
>      _app_params: Params
> +    _privileged: bool
>
>      #: Prompt to expect at the end of output when sending a command.
>      #: This is often overridden by subclasses.
> @@ -56,56 +58,63 @@ class InteractiveShell(ABC):
>      #: Path to the executable to start the interactive application.
>      path: ClassVar[PurePath]
>
> -    #: Whether this application is a DPDK app. If it is, the build directory
> -    #: for DPDK on the node will be prepended to the path to the executable.
> -    dpdk_app: ClassVar[bool] = False
> -
>      def __init__(
>          self,
> -        interactive_session: SSHClient,
> -        logger: DTSLogger,
> -        get_privileged_command: Callable[[str], str] | None,
> -        app_params: Params = Params(),
> +        node: Node,
> +        privileged: bool = False,
>          timeout: float = SETTINGS.timeout,
> +        start_on_init: bool = True,
> +        app_params: Params = Params(),
>      ) -> None:
>          """Create an SSH channel during initialization.
>
>          Args:
> -            interactive_session: The SSH session dedicated to interactive shells.
> -            logger: The logger instance this session will use.
> -            get_privileged_command: A method for modifying a command to allow it to use
> -                elevated privileges. If :data:`None`, the application will not be started
> -                with elevated privileges.
> -            app_params: The command line parameters to be passed to the application on startup.
> +            node: The node on which to run start the interactive shell.
> +            privileged: Enables the shell to run as superuser.
>              timeout: The timeout used for the SSH channel that is dedicated to this interactive
>                  shell. This timeout is for collecting output, so if reading from the buffer
>                  and no output is gathered within the timeout, an exception is thrown.
> +            start_on_init: Start interactive shell automatically after object initialisation.
> +            app_params: The command line parameters to be passed to the application on startup.
>          """
> -        self._interactive_session = interactive_session
> -        self._ssh_channel = self._interactive_session.invoke_shell()
> +        self._node = node
> +        self._logger = node._logger
> +        self._app_params = app_params
> +        self._privileged = privileged
> +        self._timeout = timeout
> +        # Ensure path is properly formatted for the host
> +        self._update_path(self._node.main_session.join_remote_path(self.path))
> +
> +        self._post_init()
> +
> +        if start_on_init:
> +            self.start_application()
> +
> +    def _post_init(self):
> +        """Overridable. Method called after the object init and before application start."""
> +
> +    def _setup_ssh_channel(self):
> +        self._ssh_channel = self._node.main_session.interactive_session.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.settimeout(self._timeout)
>          self._ssh_channel.set_combine_stderr(True)  # combines stdout and stderr streams
> -        self._logger = logger
> -        self._timeout = timeout
> -        self._app_params = app_params
> -        self._start_application(get_privileged_command)
>
> -    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
> +    def _make_start_command(self) -> str:
> +        """Makes the command that starts the interactive shell."""
> +        start_command = f"{self.path} {self._app_params or ''}"
> +        if self._privileged:
> +            start_command = self._node.main_session._get_privileged_command(start_command)
> +        return start_command
> +
> +    def start_application(self) -> None:
>          """Starts a new interactive application based on the path to the app.
>
>          This method is often overridden by subclasses as their process for
>          starting may look different.
> -
> -        Args:
> -            get_privileged_command: A function (but could be any callable) that produces
> -                the version of the command with elevated privileges.
>          """
> -        start_command = f"{self.path} {self._app_params}"
> -        if get_privileged_command is not None:
> -            start_command = get_privileged_command(start_command)
> -        self.send_command(start_command)
> +        self._setup_ssh_channel()
> +        self.send_command(self._make_start_command())
>
>      def send_command(
>          self, command: str, prompt: str | None = None, skip_first_line: bool = False
> @@ -156,3 +165,7 @@ def close(self) -> None:
>      def __del__(self) -> None:
>          """Make sure the session is properly closed before deleting the object."""
>          self.close()
> +
> +    @classmethod
> +    def _update_path(cls, path: PurePath) -> None:
> +        cls.path = path
> diff --git a/dts/framework/remote_session/python_shell.py b/dts/framework/remote_session/python_shell.py
> index ccfd3783e8..953ed100df 100644
> --- a/dts/framework/remote_session/python_shell.py
> +++ b/dts/framework/remote_session/python_shell.py
> @@ -6,9 +6,7 @@
>  Typical usage example in a TestSuite::
>
>      from framework.remote_session import PythonShell
> -    python_shell = self.tg_node.create_interactive_shell(
> -        PythonShell, timeout=5, privileged=True
> -    )
> +    python_shell = PythonShell(self.tg_node, timeout=5, privileged=True)
>      python_shell.send_command("print('Hello World')")
>      python_shell.close()
>  """
> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
> index 82701a9839..8ee6829067 100644
> --- a/dts/framework/remote_session/testpmd_shell.py
> +++ b/dts/framework/remote_session/testpmd_shell.py
> @@ -7,9 +7,7 @@
>
>  Typical usage example in a TestSuite::
>
> -    testpmd_shell = self.sut_node.create_interactive_shell(
> -            TestPmdShell, privileged=True
> -        )
> +    testpmd_shell = TestPmdShell(self.sut_node)
>      devices = testpmd_shell.get_devices()
>      for device in devices:
>          print(device)
> @@ -21,18 +19,19 @@
>  from dataclasses import dataclass, field
>  from enum import Flag, auto
>  from pathlib import PurePath
> -from typing import Callable, ClassVar
> +from typing import ClassVar
>
>  from typing_extensions import Self
>
>  from framework.exception import InteractiveCommandExecutionError
>  from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
>  from framework.parser import ParserFn, TextParser
> +from framework.remote_session.dpdk_shell import DPDKShell
>  from framework.settings import SETTINGS
> +from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
> +from framework.testbed_model.sut_node import SutNode
>  from framework.utils import StrEnum
>
> -from .interactive_shell import InteractiveShell
> -
>
>  class TestPmdDevice(object):
>      """The data of a device that testpmd can recognize.
> @@ -577,52 +576,48 @@ class TestPmdPortStats(TextParser):
>      tx_bps: int = field(metadata=TextParser.find_int(r"Tx-bps:\s+(\d+)"))
>
>
> -class TestPmdShell(InteractiveShell):
> +class TestPmdShell(DPDKShell):
>      """Testpmd interactive shell.
>
>      The testpmd shell users should never use
>      the :meth:`~.interactive_shell.InteractiveShell.send_command` method directly, but rather
>      call specialized methods. If there isn't one that satisfies a need, it should be added.
> -
> -    Attributes:
> -        number_of_ports: The number of ports which were allowed on the command-line when testpmd
> -            was started.
>      """
>
> -    number_of_ports: int
> +    _app_params: TestPmdParams
>
>      #: The path to the testpmd executable.
>      path: ClassVar[PurePath] = PurePath("app", "dpdk-testpmd")
>
> -    #: Flag this as a DPDK app so that it's clear this is not a system app and
> -    #: needs to be looked in a specific path.
> -    dpdk_app: ClassVar[bool] = True
> -
>      #: The testpmd's prompt.
>      _default_prompt: ClassVar[str] = "testpmd>"
>
>      #: This forces the prompt to appear after sending a command.
>      _command_extra_chars: ClassVar[str] = "\n"
>
> -    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
> -        """Overrides :meth:`~.interactive_shell._start_application`.
> -
> -        Add flags for starting testpmd in interactive mode and disabling messages for link state
> -        change events before starting the application. Link state is verified before starting
> -        packet forwarding and the messages create unexpected newlines in the terminal which
> -        complicates output collection.
> -
> -        Also find the number of pci addresses which were allowed on the command line when the app
> -        was started.
> -        """
> -        assert isinstance(self._app_params, TestPmdParams)
> -
> -        self.number_of_ports = (
> -            len(self._app_params.ports) if self._app_params.ports is not None else 0
> +    def __init__(
> +        self,
> +        node: SutNode,
> +        privileged: bool = True,
> +        timeout: float = SETTINGS.timeout,
> +        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
> +        ascending_cores: bool = True,
> +        append_prefix_timestamp: bool = True,
> +        start_on_init: bool = True,
> +        **app_params,
> +    ) -> None:
> +        """Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs."""
> +        super().__init__(
> +            node,
> +            privileged,
> +            timeout,
> +            lcore_filter_specifier,
> +            ascending_cores,
> +            append_prefix_timestamp,
> +            start_on_init,
> +            TestPmdParams(**app_params),
>          )
>
> -        super()._start_application(get_privileged_command)
> -
>      def start(self, verify: bool = True) -> None:
>          """Start packet forwarding with the current configuration.
>
> @@ -642,7 +637,8 @@ def start(self, verify: bool = True) -> None:
>                  self._logger.debug(f"Failed to start packet forwarding: \n{start_cmd_output}")
>                  raise InteractiveCommandExecutionError("Testpmd failed to start packet forwarding.")
>
> -            for port_id in range(self.number_of_ports):
> +            number_of_ports = len(self._app_params.ports or [])
> +            for port_id in range(number_of_ports):
>                  if not self.wait_link_status_up(port_id):
>                      raise InteractiveCommandExecutionError(
>                          "Not all ports came up after starting packet forwarding in testpmd."
> diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
> index 6af4f25a3c..88395faabe 100644
> --- a/dts/framework/testbed_model/node.py
> +++ b/dts/framework/testbed_model/node.py
> @@ -15,7 +15,7 @@
>
>  from abc import ABC
>  from ipaddress import IPv4Interface, IPv6Interface
> -from typing import Any, Callable, Type, Union
> +from typing import Any, Callable, Union
>
>  from framework.config import (
>      OS,
> @@ -25,7 +25,6 @@
>  )
>  from framework.exception import ConfigurationError
>  from framework.logger import DTSLogger, get_dts_logger
> -from framework.params import Params
>  from framework.settings import SETTINGS
>
>  from .cpu import (
> @@ -36,7 +35,7 @@
>      lcore_filter,
>  )
>  from .linux_session import LinuxSession
> -from .os_session import InteractiveShellType, OSSession
> +from .os_session import OSSession
>  from .port import Port
>  from .virtual_device import VirtualDevice
>
> @@ -196,37 +195,6 @@ def create_session(self, name: str) -> OSSession:
>          self._other_sessions.append(connection)
>          return connection
>
> -    def create_interactive_shell(
> -        self,
> -        shell_cls: Type[InteractiveShellType],
> -        timeout: float = SETTINGS.timeout,
> -        privileged: bool = False,
> -        app_params: Params = Params(),
> -    ) -> InteractiveShellType:
> -        """Factory for interactive session handlers.
> -
> -        Instantiate `shell_cls` according to the remote OS specifics.
> -
> -        Args:
> -            shell_cls: The class of the shell.
> -            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.
> -            privileged: Whether to run the shell with administrative privileges.
> -            app_args: The arguments to be passed to the application.
> -
> -        Returns:
> -            An instance of the desired interactive application shell.
> -        """
> -        if not shell_cls.dpdk_app:
> -            shell_cls.path = self.main_session.join_remote_path(shell_cls.path)
> -
> -        return self.main_session.create_interactive_shell(
> -            shell_cls,
> -            timeout,
> -            privileged,
> -            app_params,
> -        )
> -
>      def filter_lcores(
>          self,
>          filter_specifier: LogicalCoreCount | LogicalCoreList,
> diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
> index e5f5fcbe0e..e7e6c9d670 100644
> --- a/dts/framework/testbed_model/os_session.py
> +++ b/dts/framework/testbed_model/os_session.py
> @@ -26,18 +26,16 @@
>  from collections.abc import Iterable
>  from ipaddress import IPv4Interface, IPv6Interface
>  from pathlib import PurePath
> -from typing import Type, TypeVar, Union
> +from typing import Union
>
>  from framework.config import Architecture, NodeConfiguration, NodeInfo
>  from framework.logger import DTSLogger
> -from framework.params import Params
>  from framework.remote_session import (
>      InteractiveRemoteSession,
>      RemoteSession,
>      create_interactive_session,
>      create_remote_session,
>  )
> -from framework.remote_session.interactive_shell import InteractiveShell
>  from framework.remote_session.remote_session import CommandResult
>  from framework.settings import SETTINGS
>  from framework.utils import MesonArgs
> @@ -45,8 +43,6 @@
>  from .cpu import LogicalCore
>  from .port import Port
>
> -InteractiveShellType = TypeVar("InteractiveShellType", bound=InteractiveShell)
> -
>
>  class OSSession(ABC):
>      """OS-unaware to OS-aware translation API definition.
> @@ -131,36 +127,6 @@ def send_command(
>
>          return self.remote_session.send_command(command, timeout, verify, env)
>
> -    def create_interactive_shell(
> -        self,
> -        shell_cls: Type[InteractiveShellType],
> -        timeout: float,
> -        privileged: bool,
> -        app_args: Params,
> -    ) -> InteractiveShellType:
> -        """Factory for interactive session handlers.
> -
> -        Instantiate `shell_cls` according to the remote OS specifics.
> -
> -        Args:
> -            shell_cls: The class of the shell.
> -            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.
> -            privileged: Whether to run the shell with administrative privileges.
> -            app_args: The arguments to be passed to the application.
> -
> -        Returns:
> -            An instance of the desired interactive application shell.
> -        """
> -        return shell_cls(
> -            self.interactive_session.session,
> -            self._logger,
> -            self._get_privileged_command if privileged else None,
> -            app_args,
> -            timeout,
> -        )
> -
>      @staticmethod
>      @abstractmethod
>      def _get_privileged_command(command: str) -> str:
> diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
> index 83ad06ae2d..d231a01425 100644
> --- a/dts/framework/testbed_model/sut_node.py
> +++ b/dts/framework/testbed_model/sut_node.py
> @@ -16,7 +16,6 @@
>  import tarfile
>  import time
>  from pathlib import PurePath
> -from typing import Type
>
>  from framework.config import (
>      BuildTargetConfiguration,
> @@ -24,17 +23,13 @@
>      NodeInfo,
>      SutNodeConfiguration,
>  )
> -from framework.params import Params, Switch
>  from framework.params.eal import EalParams
>  from framework.remote_session.remote_session import CommandResult
>  from framework.settings import SETTINGS
>  from framework.utils import MesonArgs
>
> -from .cpu import LogicalCoreCount, LogicalCoreList
>  from .node import Node
> -from .os_session import InteractiveShellType, OSSession
> -from .port import Port
> -from .virtual_device import VirtualDevice
> +from .os_session import OSSession
>
>
>  class SutNode(Node):
> @@ -56,8 +51,8 @@ class SutNode(Node):
>      """
>
>      config: SutNodeConfiguration
> -    _dpdk_prefix_list: list[str]
> -    _dpdk_timestamp: str
> +    dpdk_prefix_list: list[str]
> +    dpdk_timestamp: str
>      _build_target_config: BuildTargetConfiguration | None
>      _env_vars: dict
>      _remote_tmp_dir: PurePath
> @@ -76,14 +71,14 @@ def __init__(self, node_config: SutNodeConfiguration):
>              node_config: The SUT node's test run configuration.
>          """
>          super(SutNode, self).__init__(node_config)
> -        self._dpdk_prefix_list = []
> +        self.dpdk_prefix_list = []
>          self._build_target_config = None
>          self._env_vars = {}
>          self._remote_tmp_dir = self.main_session.get_remote_tmp_dir()
>          self.__remote_dpdk_dir = None
>          self._app_compile_timeout = 90
>          self._dpdk_kill_session = None
> -        self._dpdk_timestamp = (
> +        self.dpdk_timestamp = (
>              f"{str(os.getpid())}_{time.strftime('%Y%m%d%H%M%S', time.localtime())}"
>          )
>          self._dpdk_version = None
> @@ -283,73 +278,11 @@ def kill_cleanup_dpdk_apps(self) -> None:
>          """Kill all dpdk applications on the SUT, then clean up hugepages."""
>          if self._dpdk_kill_session and self._dpdk_kill_session.is_alive():
>              # we can use the session if it exists and responds
> -            self._dpdk_kill_session.kill_cleanup_dpdk_apps(self._dpdk_prefix_list)
> +            self._dpdk_kill_session.kill_cleanup_dpdk_apps(self.dpdk_prefix_list)
>          else:
>              # otherwise, we need to (re)create it
>              self._dpdk_kill_session = self.create_session("dpdk_kill")
> -        self._dpdk_prefix_list = []
> -
> -    def create_eal_parameters(
> -        self,
> -        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
> -        ascending_cores: bool = True,
> -        prefix: str = "dpdk",
> -        append_prefix_timestamp: bool = True,
> -        no_pci: Switch = None,
> -        vdevs: list[VirtualDevice] | None = None,
> -        ports: list[Port] | None = None,
> -        other_eal_param: str = "",
> -    ) -> EalParams:
> -        """Compose the EAL parameters.
> -
> -        Process the list of cores and the DPDK prefix and pass that along with
> -        the rest of the arguments.
> -
> -        Args:
> -            lcore_filter_specifier: A number of lcores/cores/sockets to use
> -                or a list of lcore ids to use.
> -                The default will select one lcore for each of two cores
> -                on one socket, in ascending order of core ids.
> -            ascending_cores: Sort cores in ascending order (lowest to highest IDs).
> -                If :data:`False`, sort in descending order.
> -            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
> -            append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.
> -            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
> -            vdevs: Virtual devices, e.g.::
> -
> -                vdevs=[
> -                    VirtualDevice('net_ring0'),
> -                    VirtualDevice('net_ring1')
> -                ]
> -            ports: The list of ports to allow. If :data:`None`, all ports listed in `self.ports`
> -                will be allowed.
> -            other_eal_param: user defined DPDK EAL parameters, e.g.:
> -                ``other_eal_param='--single-file-segments'``.
> -
> -        Returns:
> -            An EAL param string, such as
> -            ``-c 0xf -a 0000:88:00.0 --file-prefix=dpdk_1112_20190809143420``.
> -        """
> -        lcore_list = LogicalCoreList(self.filter_lcores(lcore_filter_specifier, ascending_cores))
> -
> -        if append_prefix_timestamp:
> -            prefix = f"{prefix}_{self._dpdk_timestamp}"
> -        prefix = self.main_session.get_dpdk_file_prefix(prefix)
> -        if prefix:
> -            self._dpdk_prefix_list.append(prefix)
> -
> -        if ports is None:
> -            ports = self.ports
> -
> -        return EalParams(
> -            lcore_list=lcore_list,
> -            memory_channels=self.config.memory_channels,
> -            prefix=prefix,
> -            no_pci=no_pci,
> -            vdevs=vdevs,
> -            ports=ports,
> -            other_eal_param=Params.from_str(other_eal_param),
> -        )
> +        self.dpdk_prefix_list = []
>
>      def run_dpdk_app(
>          self, app_path: PurePath, eal_params: EalParams, timeout: float = 30
> @@ -379,49 +312,6 @@ def configure_ipv4_forwarding(self, enable: bool) -> None:
>          """
>          self.main_session.configure_ipv4_forwarding(enable)
>
> -    def create_interactive_shell(
> -        self,
> -        shell_cls: Type[InteractiveShellType],
> -        timeout: float = SETTINGS.timeout,
> -        privileged: bool = False,
> -        app_params: Params = Params(),
> -        eal_params: EalParams | None = None,
> -    ) -> InteractiveShellType:
> -        """Extend the factory for interactive session handlers.
> -
> -        The extensions are SUT node specific:
> -
> -            * The default for `eal_parameters`,
> -            * The interactive shell path `shell_cls.path` is prepended with path to the remote
> -              DPDK build directory for DPDK apps.
> -
> -        Args:
> -            shell_cls: The class of the shell.
> -            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.
> -            privileged: Whether to run the shell with administrative privileges.
> -            app_params: The parameters to be passed to the application.
> -            eal_params: List of EAL parameters to use to launch the app. If this
> -                isn't provided or an empty string is passed, it will default to calling
> -                :meth:`create_eal_parameters`.
> -
> -        Returns:
> -            An instance of the desired interactive application shell.
> -        """
> -        # We need to append the build directory and add EAL parameters for DPDK apps
> -        if shell_cls.dpdk_app:
> -            if eal_params is None:
> -                eal_params = self.create_eal_parameters()
> -            eal_params.append_str(str(app_params))
> -            app_params = eal_params
> -
> -            shell_cls.path = self.main_session.join_remote_path(
> -                self.remote_dpdk_build_dir, shell_cls.path
> -            )
> -
> -        return super().create_interactive_shell(shell_cls, timeout, privileged, app_params)
> -
>      def bind_ports_to_driver(self, for_dpdk: bool = True) -> None:
>          """Bind all ports on the SUT to a driver.
>
> diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py
> index 7bc1c2cc08..bf58ad1c5e 100644
> --- a/dts/framework/testbed_model/traffic_generator/scapy.py
> +++ b/dts/framework/testbed_model/traffic_generator/scapy.py
> @@ -217,9 +217,7 @@ def __init__(self, tg_node: Node, config: ScapyTrafficGeneratorConfig):
>              self._tg_node.config.os == OS.linux
>          ), "Linux is the only supported OS for scapy traffic generation"
>
> -        self.session = self._tg_node.create_interactive_shell(
> -            PythonShell, timeout=5, privileged=True
> -        )
> +        self.session = PythonShell(self._tg_node, timeout=5, privileged=True)
>
>          # import libs in remote python console
>          for import_statement in SCAPY_RPC_SERVER_IMPORTS:
> diff --git a/dts/tests/TestSuite_hello_world.py b/dts/tests/TestSuite_hello_world.py
> index 0d6995f260..d958f99030 100644
> --- a/dts/tests/TestSuite_hello_world.py
> +++ b/dts/tests/TestSuite_hello_world.py
> @@ -7,6 +7,7 @@
>  No other EAL parameters apart from cores are used.
>  """
>
> +from framework.remote_session.dpdk_shell import compute_eal_params
>  from framework.test_suite import TestSuite
>  from framework.testbed_model.cpu import (
>      LogicalCoreCount,
> @@ -38,7 +39,7 @@ def test_hello_world_single_core(self) -> None:
>          # get the first usable core
>          lcore_amount = LogicalCoreCount(1, 1, 1)
>          lcores = LogicalCoreCountFilter(self.sut_node.lcores, lcore_amount).filter()
> -        eal_para = self.sut_node.create_eal_parameters(lcore_filter_specifier=lcore_amount)
> +        eal_para = compute_eal_params(self.sut_node, lcore_filter_specifier=lcore_amount)
>          result = self.sut_node.run_dpdk_app(self.app_helloworld_path, eal_para)
>          self.verify(
>              f"hello from core {int(lcores[0])}" in result.stdout,
> @@ -55,8 +56,8 @@ def test_hello_world_all_cores(self) -> None:
>              "hello from core <core_id>"
>          """
>          # get the maximum logical core number
> -        eal_para = self.sut_node.create_eal_parameters(
> -            lcore_filter_specifier=LogicalCoreList(self.sut_node.lcores)
> +        eal_para = compute_eal_params(
> +            self.sut_node, lcore_filter_specifier=LogicalCoreList(self.sut_node.lcores)
>          )
>          result = self.sut_node.run_dpdk_app(self.app_helloworld_path, eal_para, 50)
>          for lcore in self.sut_node.lcores:
> diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
> index 6d206c1a40..43cf5c61eb 100644
> --- a/dts/tests/TestSuite_pmd_buffer_scatter.py
> +++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
> @@ -16,14 +16,13 @@
>  """
>
>  import struct
> -from dataclasses import asdict
>
>  from scapy.layers.inet import IP  # type: ignore[import-untyped]
>  from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
>  from scapy.packet import Raw  # type: ignore[import-untyped]
>  from scapy.utils import hexstr  # type: ignore[import-untyped]
>
> -from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
> +from framework.params.testpmd import SimpleForwardingModes
>  from framework.remote_session.testpmd_shell import TestPmdShell
>  from framework.test_suite import TestSuite
>
> @@ -103,17 +102,13 @@ def pmd_scatter(self, mbsize: int) -> None:
>          Test:
>              Start testpmd and run functional test with preset mbsize.
>          """
> -        testpmd = self.sut_node.create_interactive_shell(
> -            TestPmdShell,
> -            app_params=TestPmdParams(
> -                forward_mode=SimpleForwardingModes.mac,
> -                mbcache=200,
> -                mbuf_size=[mbsize],
> -                max_pkt_len=9000,
> -                tx_offloads=0x00008000,
> -                **asdict(self.sut_node.create_eal_parameters()),
> -            ),
> -            privileged=True,
> +        testpmd = TestPmdShell(
> +            self.sut_node,
> +            forward_mode=SimpleForwardingModes.mac,
> +            mbcache=200,
> +            mbuf_size=[mbsize],
> +            max_pkt_len=9000,
> +            tx_offloads=0x00008000,
>          )
>          testpmd.start()
>
> diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py
> index ca678f662d..eca27acfd8 100644
> --- a/dts/tests/TestSuite_smoke_tests.py
> +++ b/dts/tests/TestSuite_smoke_tests.py
> @@ -99,7 +99,7 @@ def test_devices_listed_in_testpmd(self) -> None:
>          Test:
>              List all devices found in testpmd and verify the configured devices are among them.
>          """
> -        testpmd_driver = self.sut_node.create_interactive_shell(TestPmdShell, privileged=True)
> +        testpmd_driver = TestPmdShell(self.sut_node)
>          dev_list = [str(x) for x in testpmd_driver.get_devices()]
>          for nic in self.nics_in_node:
>              self.verify(
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v5 8/8] dts: use Unpack for type checking and hinting
  2024-06-17 14:54   ` [PATCH v5 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
@ 2024-06-17 15:25     ` Nicholas Pratte
  0 siblings, 0 replies; 159+ messages in thread
From: Nicholas Pratte @ 2024-06-17 15:25 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Jeremy Spewock, Juraj Linkeš, Paul Szczepanek

Tested-by: Nicholas Pratte <npratte@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>

On Mon, Jun 17, 2024 at 10:54 AM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> Interactive shells that inherit DPDKShell initialise their params
> classes from a kwargs dict. Therefore, static type checking is
> disabled. This change uses the functionality of Unpack added in
> PEP 692 to re-enable it. The disadvantage is that this functionality has
> been implemented only with TypedDict, forcing the creation of TypedDict
> mirrors of the Params classes.
>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
> Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
> Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
> ---
>  dts/framework/params/types.py                 | 133 ++++++++++++++++++
>  dts/framework/remote_session/testpmd_shell.py |   5 +-
>  2 files changed, 136 insertions(+), 2 deletions(-)
>  create mode 100644 dts/framework/params/types.py
>
> diff --git a/dts/framework/params/types.py b/dts/framework/params/types.py
> new file mode 100644
> index 0000000000..e668f658d8
> --- /dev/null
> +++ b/dts/framework/params/types.py
> @@ -0,0 +1,133 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2024 Arm Limited
> +
> +"""Module containing TypeDict-equivalents of Params classes for static typing and hinting.
> +
> +TypedDicts can be used in conjunction with Unpack and kwargs for type hinting on function calls.
> +
> +Example:
> +    ..code:: python
> +        def create_testpmd(**kwargs: Unpack[TestPmdParamsDict]):
> +            params = TestPmdParams(**kwargs)
> +"""
> +
> +from pathlib import PurePath
> +from typing import TypedDict
> +
> +from framework.params import Switch, YesNoSwitch
> +from framework.params.testpmd import (
> +    AnonMempoolAllocationMode,
> +    EthPeer,
> +    Event,
> +    FlowGenForwardingMode,
> +    HairpinMode,
> +    NoisyForwardingMode,
> +    Params,
> +    PortNUMAConfig,
> +    PortTopology,
> +    RingNUMAConfig,
> +    RSSSetting,
> +    RXMultiQueueMode,
> +    RXRingParams,
> +    SimpleForwardingModes,
> +    SimpleMempoolAllocationMode,
> +    TxIPAddrPair,
> +    TXOnlyForwardingMode,
> +    TXRingParams,
> +    TxUDPPortPair,
> +)
> +from framework.testbed_model.cpu import LogicalCoreList
> +from framework.testbed_model.port import Port
> +from framework.testbed_model.virtual_device import VirtualDevice
> +
> +
> +class EalParamsDict(TypedDict, total=False):
> +    """:class:`TypedDict` equivalent of :class:`~.eal.EalParams`."""
> +
> +    lcore_list: LogicalCoreList | None
> +    memory_channels: int | None
> +    prefix: str
> +    no_pci: Switch
> +    vdevs: list[VirtualDevice] | None
> +    ports: list[Port] | None
> +    other_eal_param: Params | None
> +
> +
> +class TestPmdParamsDict(EalParamsDict, total=False):
> +    """:class:`TypedDict` equivalent of :class:`~.testpmd.TestPmdParams`."""
> +
> +    interactive_mode: Switch
> +    auto_start: Switch
> +    tx_first: Switch
> +    stats_period: int | None
> +    display_xstats: list[str] | None
> +    nb_cores: int | None
> +    coremask: int | None
> +    nb_ports: int | None
> +    port_topology: PortTopology | None
> +    portmask: int | None
> +    portlist: str | None
> +    numa: YesNoSwitch
> +    socket_num: int | None
> +    port_numa_config: list[PortNUMAConfig] | None
> +    ring_numa_config: list[RingNUMAConfig] | None
> +    total_num_mbufs: int | None
> +    mbuf_size: list[int] | None
> +    mbcache: int | None
> +    max_pkt_len: int | None
> +    eth_peers_configfile: PurePath | None
> +    eth_peer: list[EthPeer] | None
> +    tx_ip: TxIPAddrPair | None
> +    tx_udp: TxUDPPortPair | None
> +    enable_lro: Switch
> +    max_lro_pkt_size: int | None
> +    disable_crc_strip: Switch
> +    enable_scatter: Switch
> +    enable_hw_vlan: Switch
> +    enable_hw_vlan_filter: Switch
> +    enable_hw_vlan_strip: Switch
> +    enable_hw_vlan_extend: Switch
> +    enable_hw_qinq_strip: Switch
> +    pkt_drop_enabled: Switch
> +    rss: RSSSetting | None
> +    forward_mode: (
> +        SimpleForwardingModes
> +        | FlowGenForwardingMode
> +        | TXOnlyForwardingMode
> +        | NoisyForwardingMode
> +        | None
> +    )
> +    hairpin_mode: HairpinMode | None
> +    hairpin_queues: int | None
> +    burst: int | None
> +    enable_rx_cksum: Switch
> +    rx_queues: int | None
> +    rx_ring: RXRingParams | None
> +    no_flush_rx: Switch
> +    rx_segments_offsets: list[int] | None
> +    rx_segments_length: list[int] | None
> +    multi_rx_mempool: Switch
> +    rx_shared_queue: Switch | int
> +    rx_offloads: int | None
> +    rx_mq_mode: RXMultiQueueMode | None
> +    tx_queues: int | None
> +    tx_ring: TXRingParams | None
> +    tx_offloads: int | None
> +    eth_link_speed: int | None
> +    disable_link_check: Switch
> +    disable_device_start: Switch
> +    no_lsc_interrupt: Switch
> +    no_rmv_interrupt: Switch
> +    bitrate_stats: int | None
> +    latencystats: int | None
> +    print_events: list[Event] | None
> +    mask_events: list[Event] | None
> +    flow_isolate_all: Switch
> +    disable_flow_flush: Switch
> +    hot_plug: Switch
> +    vxlan_gpe_port: int | None
> +    geneve_parsed_port: int | None
> +    lock_all_memory: YesNoSwitch
> +    mempool_allocation_mode: SimpleMempoolAllocationMode | AnonMempoolAllocationMode | None
> +    record_core_cycles: Switch
> +    record_burst_status: Switch
> diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
> index 8ee6829067..96a690b6de 100644
> --- a/dts/framework/remote_session/testpmd_shell.py
> +++ b/dts/framework/remote_session/testpmd_shell.py
> @@ -21,10 +21,11 @@
>  from pathlib import PurePath
>  from typing import ClassVar
>
> -from typing_extensions import Self
> +from typing_extensions import Self, Unpack
>
>  from framework.exception import InteractiveCommandExecutionError
>  from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
> +from framework.params.types import TestPmdParamsDict
>  from framework.parser import ParserFn, TextParser
>  from framework.remote_session.dpdk_shell import DPDKShell
>  from framework.settings import SETTINGS
> @@ -604,7 +605,7 @@ def __init__(
>          ascending_cores: bool = True,
>          append_prefix_timestamp: bool = True,
>          start_on_init: bool = True,
> -        **app_params,
> +        **app_params: Unpack[TestPmdParamsDict],
>      ) -> None:
>          """Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs."""
>          super().__init__(
> --
> 2.34.1
>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v2 1/8] dts: add params manipulation module
  2024-06-17 11:44       ` Luca Vizzarro
@ 2024-06-18  8:55         ` Juraj Linkeš
  0 siblings, 0 replies; 159+ messages in thread
From: Juraj Linkeš @ 2024-06-18  8:55 UTC (permalink / raw)
  To: Luca Vizzarro, dev; +Cc: Jeremy Spewock, Paul Szczepanek



On 17. 6. 2024 13:44, Luca Vizzarro wrote:
> On 06/06/2024 10:19, Juraj Linkeš wrote:
>> The static method in the other patch is called compose and does 
>> essentially the same thing, right? Can we use the same name (or a 
>> similar one)?
>>
>> Also, what is the difference in approaches between the two patches 
>> (or, more accurately, the reason behind the difference)? In the other 
>> patch, we're returning a dict, here we're returning a function directly.
> 
> They are essentially different. This one is as it's called quite plainly 
> a function reduction. Can be used in any context in reality.
> 
> The other one is tighter and has some specific controls (exit early if 
> None), and is directly represented as a dictionary as that's the only 
> intended way of consumption.
> 

This is a generic explanation. I had to go back to the code to find the 
reason for the difference. The dict is tailored to be used with the 
dataclass field's metadata argument and the reduce function is used with 
functions gotten from the metadata. At this point, the question is, why 
the difference when it seems we're trying to do the same thing (apply 
multiple functions through metadata), but slightly differently?

>>> +    """Reduces an iterable of :attr:`FnPtr` from end to start to a 
>>> composite function.
>>
>> We should make the order of application the same as in the method in 
>> other patch, so if we change the order in the first one, we should do 
>> the same here.
> 
> While I don't think it feels any natural coding-wise (yet again, a 
> matter of common readability to the developer vs how it's actually run), 
> I won't object as I don't have a preference.
> 
>>> +
>>> +    If the iterable is empty, the created function just returns its 
>>> fed value back.
>>> +    """
>>> +
>>> +    def composite_function(value: Any):
>>
>> The return type is missing.
> 
> I will remove types from the decorator functions as it adds too much 
> complexity and little advantage. I wasn't able to easily resolve with 
> mypy. Especially in conjunction with modify_str, where mypy complains a 
> lot about __str__ being treated as an unbound method/plain function that 
> doesn't take self.
> 

We can make this a policy - don't add return types to decorators. But 
I'm curious, what does mypy say when you use just Callable?

>>> +    _suffix = ""
>>> +    """Holder of the plain text value of Params when called 
>>> directly. A suffix for child classes."""
>>> +
>>> +    """========= BEGIN FIELD METADATA MODIFIER FUNCTIONS ========"""
>>> +
>>> +    @staticmethod
>>> +    def value_only() -> ParamsModifier:
>>
>> As far as I (or my IDE) can tell, this is not used anywhere. What's 
>> the purpose of this?
> 
> I guess this is no longer in use at the moment. It could still be used 
> for positional arguments in the future. But will remove for now.
> 
>>> +    def append_str(self, text: str) -> None:
>>> +        """Appends a string at the end of the string representation."""
>>> +        self._suffix += text
>>> +
>>> +    def __iadd__(self, text: str) -> Self:
>>> +        """Appends a string at the end of the string representation."""
>>> +        self.append_str(text)
>>> +        return self
>>> +
>>> +    @classmethod
>>> +    def from_str(cls, text: str) -> Self:
>>
>> I tried to figure out how self._suffix is used and I ended up finding 
>> out this method is not used anywhere. Is that correct? If it's not 
>> used, let's remove it.
> 
> It is used through Params.from_str. It is used transitionally in the 
> next commits.
> 

Can we remove it with the last usage of it being removed? As far as I 
understand, there's no need for the method with all commits applied.

>> What actually should be the suffix? A an arbitrary string that gets 
>> appended to the rendered command line arguments? I guess this would be 
>> here so that we can pass an already rendered string?
> 
> As we already discussed and agreed before, this is just to use Params 
> plainly with arbitrary text. It is treated as a suffix, because if 
> Params is inherited this stays, and then it can either be a prefix or a 
> suffix in that case.
> 
>>> +        """Creates a plain Params object from a string."""
>>> +        obj = cls()
>>> +        obj.append_str(text)
>>> +        return obj
>>> +
>>> +    @staticmethod
>>> +    def _make_switch(
>>> +        name: str, is_short: bool = False, is_no: bool = False, 
>>> value: str | None = None
>>> +    ) -> str:
>>> +        prefix = f"{'-' if is_short else '--'}{'no-' if is_no else ''}"
>>
>> Does is_short work with is_no (that is, if both are True)? Do we need 
>> to worry about it if not?
> 
> They should not work together, and no it is not enforced. But finally 
> that's up to the command line interface we are modelling the parameters 
> against. It is not a problem in reality.
> 

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v3 7/8] dts: rework interactive shells
  2024-06-17 12:13       ` Luca Vizzarro
@ 2024-06-18  9:18         ` Juraj Linkeš
  0 siblings, 0 replies; 159+ messages in thread
From: Juraj Linkeš @ 2024-06-18  9:18 UTC (permalink / raw)
  To: Luca Vizzarro, dev; +Cc: Jeremy Spewock, Paul Szczepanek



On 17. 6. 2024 14:13, Luca Vizzarro wrote:
> On 06/06/2024 19:03, Juraj Linkeš wrote:
>>> +class DPDKShell(InteractiveShell, ABC):
>>> +    """The base class for managing DPDK-based interactive shells.
>>> +
>>> +    This class shouldn't be instantiated directly, but instead be 
>>> extended.
>>> +    It automatically injects computed EAL parameters based on the 
>>> node in the
>>> +    supplied app parameters.
>>> +    """
>>> +
>>> +    _node: SutNode
>>
>> Same here, better to be explicit with _sut_node.
> 
> This should not be changed as it's just overriding the type of the 
> parent's attribute.
> 

Ah, right, thanks for pointing this out.

>>> +    _app_params: EalParams
>>> +
>>> +    _lcore_filter_specifier: LogicalCoreCount | LogicalCoreList
>>> +    _ascending_cores: bool
>>> +    _append_prefix_timestamp: bool
>>> +
>>> +    def __init__(
>>> +        self,
>>> +        node: SutNode,
>>> +        app_params: EalParams,
>>> +        privileged: bool = True,
>>> +        timeout: float = SETTINGS.timeout,
>>> +        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = 
>>> LogicalCoreCount(),
>>> +        ascending_cores: bool = True,
>>> +        append_prefix_timestamp: bool = True,
>>> +        start_on_init: bool = True,
>>> +    ) -> None:
>>> +        """Overrides 
>>> :meth:`~.interactive_shell.InteractiveShell.__init__`."""

I just noticed this says Overrides, but it should be extends with a 
brief note on what it's extending the constructor with.

>>> +        self._lcore_filter_specifier = lcore_filter_specifier
>>> +        self._ascending_cores = ascending_cores
>>> +        self._append_prefix_timestamp = append_prefix_timestamp
>>> +
>>> +        super().__init__(node, app_params, privileged, timeout, 
>>> start_on_init)
>>> +
>>> +    def _post_init(self):
>>> +        """Computes EAL params based on the node capabilities before 
>>> start."""
>>
>> We could just put this before calling super().__init__() in this class 
>> if we update path some other way, right? It's probably better to 
>> override the class method (_update_path()) in subclasses than having 
>> this _post_init() method.
> 
> It's more complicated than this. The ultimate parent (InteractiveShell) 
> is what sets all the common attributes. This needs to happen before 
> compute_eal_params and updating the path with the dpdk folder. This 
> wouldn't be a problem if we were to call super().__init__ at the 
> beginning. But that way we'd lose the ability to automatically start the 
> shell though.

Yes, that's why I proposed a solution that takes that into account. :-)

If we override _update_path() and change it to a regular method, we can 
just do:

         self._lcore_filter_specifier = lcore_filter_specifier
         self._ascending_cores = ascending_cores
         self._append_prefix_timestamp = append_prefix_timestamp

         # Here it's clear we don't need the parent to set the 
attributes as we have access to them.
	self._app_params = compute_eal_params(
             node,
             app_params,
             lcore_filter_specifier,
             ascending_cores,
             append_prefix_timestamp,
         )

         super().__init__(node, app_params, privileged, timeout, 
start_on_init)

     # no longer a class method, it'll create the path instance variable 
(starting with the value assigned to the path class variable) and we can 
access self._node
     def _update_path(self, path: PurePath):
         """Updates the path."""
         self.path = self._node.remote_dpdk_build_dir.joinpath(self.path))


As far as I can tell, this does the same thing without using _post_init 
and has the added benefit of overriding what we expect the subclasses to 
override (the path, as that's what should be different across shells).

^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v6 0/8] dts: add testpmd params
  2024-03-26 19:04 [PATCH 0/6] dts: add testpmd params and statefulness Luca Vizzarro
                   ` (9 preceding siblings ...)
  2024-06-17 14:54 ` [PATCH v5 0/8] dts: add testpmd params Luca Vizzarro
@ 2024-06-19 10:23 ` Luca Vizzarro
  2024-06-19 10:23   ` [PATCH v6 1/8] dts: add params manipulation module Luca Vizzarro
                     ` (7 more replies)
  2024-06-19 14:02 ` [PATCH v7 0/8] dts: add testpmd params Luca Vizzarro
  11 siblings, 8 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-19 10:23 UTC (permalink / raw)
  To: dev; +Cc: Jeremy Spewock, Juraj Linkeš, Luca Vizzarro

v6:
- refactored InteractiveShell and DPDKShell constructors
- fixed docstrings
- removed some more module-wide imports
v5:
- fixed typo
v4:
- fixed up docstrings
- made refactoring changes
- removed params value only
- rebased on top of show port info/stats
v3:
- refactored InteractiveShell methods
- fixed docstrings
v2:
- refactored the params module
- strengthened typing of the params module
- moved the params module into its own package
- refactored EalParams and TestPmdParams and
  moved under the params package
- reworked interactions between nodes and shells
- refactored imports leading to circular dependencies

---
Depends-on: series-32112 ("dts: testpmd show port info/stats")
---

Luca Vizzarro (8):
  dts: add params manipulation module
  dts: use Params for interactive shells
  dts: refactor EalParams
  dts: remove module-wide imports
  dts: add testpmd shell params
  dts: use testpmd params for scatter test suite
  dts: rework interactive shells
  dts: use Unpack for type checking and hinting

 dts/framework/params/__init__.py              | 359 +++++++++++
 dts/framework/params/eal.py                   |  50 ++
 dts/framework/params/testpmd.py               | 607 ++++++++++++++++++
 dts/framework/params/types.py                 | 133 ++++
 dts/framework/remote_session/__init__.py      |   7 +-
 dts/framework/remote_session/dpdk_shell.py    | 105 +++
 .../remote_session/interactive_shell.py       |  79 ++-
 dts/framework/remote_session/python_shell.py  |   4 +-
 dts/framework/remote_session/testpmd_shell.py |  99 +--
 dts/framework/runner.py                       |   4 +-
 dts/framework/test_suite.py                   |   9 +-
 dts/framework/testbed_model/__init__.py       |   9 -
 dts/framework/testbed_model/node.py           |  36 +-
 dts/framework/testbed_model/os_session.py     |  38 +-
 dts/framework/testbed_model/sut_node.py       | 194 +-----
 dts/framework/testbed_model/tg_node.py        |   9 +-
 .../traffic_generator/__init__.py             |   7 +-
 .../testbed_model/traffic_generator/scapy.py  |   6 +-
 dts/tests/TestSuite_hello_world.py            |   9 +-
 dts/tests/TestSuite_pmd_buffer_scatter.py     |  21 +-
 dts/tests/TestSuite_smoke_tests.py            |   4 +-
 21 files changed, 1388 insertions(+), 401 deletions(-)
 create mode 100644 dts/framework/params/__init__.py
 create mode 100644 dts/framework/params/eal.py
 create mode 100644 dts/framework/params/testpmd.py
 create mode 100644 dts/framework/params/types.py
 create mode 100644 dts/framework/remote_session/dpdk_shell.py

-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v6 1/8] dts: add params manipulation module
  2024-06-19 10:23 ` [PATCH v6 0/8] dts: add testpmd params Luca Vizzarro
@ 2024-06-19 10:23   ` Luca Vizzarro
  2024-06-19 12:45     ` Juraj Linkeš
  2024-06-19 10:23   ` [PATCH v6 2/8] dts: use Params for interactive shells Luca Vizzarro
                     ` (6 subsequent siblings)
  7 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-19 10:23 UTC (permalink / raw)
  To: dev; +Cc: Jeremy Spewock, Juraj Linkeš, Luca Vizzarro, Paul Szczepanek

This commit introduces a new "params" module, which adds a new way
to manage command line parameters. The provided Params dataclass
is able to read the fields of its child class and produce a string
representation to supply to the command line. Any data structure
that is intended to represent command line parameters can inherit it.

The main purpose is to make it easier to represent data structures that
map to parameters. Aiding quicker development, while minimising code
bloat.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 dts/framework/params/__init__.py | 359 +++++++++++++++++++++++++++++++
 1 file changed, 359 insertions(+)
 create mode 100644 dts/framework/params/__init__.py

diff --git a/dts/framework/params/__init__.py b/dts/framework/params/__init__.py
new file mode 100644
index 0000000000..5a6fd93053
--- /dev/null
+++ b/dts/framework/params/__init__.py
@@ -0,0 +1,359 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Parameter manipulation module.
+
+This module provides :class:`Params` which can be used to model any data structure
+that is meant to represent any command line parameters.
+"""
+
+from dataclasses import dataclass, fields
+from enum import Flag
+from typing import (
+    Any,
+    Callable,
+    Iterable,
+    Literal,
+    Reversible,
+    TypedDict,
+    TypeVar,
+    cast,
+)
+
+from typing_extensions import Self
+
+T = TypeVar("T")
+
+#: Type for a function taking one argument.
+FnPtr = Callable[[Any], Any]
+#: Type for a switch parameter.
+Switch = Literal[True, None]
+#: Type for a yes/no switch parameter.
+YesNoSwitch = Literal[True, False, None]
+
+
+def _reduce_functions(funcs: Iterable[FnPtr]) -> FnPtr:
+    """Reduces an iterable of :attr:`FnPtr` from left to right to a single function.
+
+    If the iterable is empty, the created function just returns its fed value back.
+
+    Args:
+        funcs: An iterable containing the functions to be chained from left to right.
+
+    Returns:
+        FnPtr: A function that calls the given functions from left to right.
+    """
+
+    def reduced_fn(value):
+        for fn in funcs:
+            value = fn(value)
+        return value
+
+    return reduced_fn
+
+
+def modify_str(*funcs: FnPtr) -> Callable[[T], T]:
+    """Class decorator modifying the ``__str__`` method with a function created from its arguments.
+
+    The :attr:`FnPtr`s fed to the decorator are executed from left to right in the arguments list
+    order.
+
+    Args:
+        *funcs: The functions to chain from left to right.
+
+    Returns:
+        The decorator.
+
+    Example:
+        .. code:: python
+
+            @convert_str(hex_from_flag_value)
+            class BitMask(enum.Flag):
+                A = auto()
+                B = auto()
+
+        will allow ``BitMask`` to render as a hexadecimal value.
+    """
+
+    def _class_decorator(original_class):
+        original_class.__str__ = _reduce_functions(funcs)
+        return original_class
+
+    return _class_decorator
+
+
+def comma_separated(values: Iterable[Any]) -> str:
+    """Converts an iterable into a comma-separated string.
+
+    Args:
+        values: An iterable of objects.
+
+    Returns:
+        A comma-separated list of stringified values.
+    """
+    return ",".join([str(value).strip() for value in values if value is not None])
+
+
+def bracketed(value: str) -> str:
+    """Adds round brackets to the input.
+
+    Args:
+        value: Any string.
+
+    Returns:
+        A string surrounded by round brackets.
+    """
+    return f"({value})"
+
+
+def str_from_flag_value(flag: Flag) -> str:
+    """Returns the value from a :class:`enum.Flag` as a string.
+
+    Args:
+        flag: An instance of :class:`Flag`.
+
+    Returns:
+        The stringified value of the given flag.
+    """
+    return str(flag.value)
+
+
+def hex_from_flag_value(flag: Flag) -> str:
+    """Returns the value from a :class:`enum.Flag` converted to hexadecimal.
+
+    Args:
+        flag: An instance of :class:`Flag`.
+
+    Returns:
+        The value of the given flag in hexadecimal representation.
+    """
+    return hex(flag.value)
+
+
+class ParamsModifier(TypedDict, total=False):
+    """Params modifiers dict compatible with the :func:`dataclasses.field` metadata parameter."""
+
+    #:
+    Params_short: str
+    #:
+    Params_long: str
+    #:
+    Params_multiple: bool
+    #:
+    Params_convert_value: Reversible[FnPtr]
+
+
+@dataclass
+class Params:
+    """Dataclass that renders its fields into command line arguments.
+
+    The parameter name is taken from the field name by default. The following:
+
+    .. code:: python
+
+        name: str | None = "value"
+
+    is rendered as ``--name=value``.
+
+    Through :func:`dataclasses.field` the resulting parameter can be manipulated by applying
+    this class' metadata modifier functions. These return regular dictionaries which can be combined
+    together using the pipe (OR) operator, as used in the example for :meth:`~Params.multiple`.
+
+    To use fields as switches, set the value to ``True`` to render them. If you
+    use a yes/no switch you can also set ``False`` which would render a switch
+    prefixed with ``--no-``. Examples:
+
+    .. code:: python
+
+        interactive: Switch = True  # renders --interactive
+        numa: YesNoSwitch   = False # renders --no-numa
+
+    Setting ``None`` will prevent it from being rendered. The :attr:`~Switch` type alias is provided
+    for regular switches, whereas :attr:`~YesNoSwitch` is offered for yes/no ones.
+
+    An instance of a dataclass inheriting ``Params`` can also be assigned to an attribute,
+    this helps with grouping parameters together.
+    The attribute holding the dataclass will be ignored and the latter will just be rendered as
+    expected.
+    """
+
+    _suffix = ""
+    """Holder of the plain text value of Params when called directly. A suffix for child classes."""
+
+    """========= BEGIN FIELD METADATA MODIFIER FUNCTIONS ========"""
+
+    @staticmethod
+    def short(name: str) -> ParamsModifier:
+        """Overrides any parameter name with the given short option.
+
+        Args:
+            name: The short parameter name.
+
+        Returns:
+            ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
+                the parameter short name modifier.
+
+        Example:
+            .. code:: python
+
+                logical_cores: str | None = field(default="1-4", metadata=Params.short("l"))
+
+            will render as ``-l=1-4`` instead of ``--logical-cores=1-4``.
+        """
+        return ParamsModifier(Params_short=name)
+
+    @staticmethod
+    def long(name: str) -> ParamsModifier:
+        """Overrides the inferred parameter name to the specified one.
+
+        Args:
+            name: The long parameter name.
+
+        Returns:
+            ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
+                the parameter long name modifier.
+
+        Example:
+            .. code:: python
+
+                x_name: str | None = field(default="y", metadata=Params.long("x"))
+
+            will render as ``--x=y``, but the field is accessed and modified through ``x_name``.
+        """
+        return ParamsModifier(Params_long=name)
+
+    @staticmethod
+    def multiple() -> ParamsModifier:
+        """Specifies that this parameter is set multiple times. The parameter type must be a list.
+
+        Returns:
+            ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
+                the multiple parameters modifier.
+
+        Example:
+            .. code:: python
+
+                ports: list[int] | None = field(
+                    default_factory=lambda: [0, 1, 2],
+                    metadata=Params.multiple() | Params.long("port")
+                )
+
+            will render as ``--port=0 --port=1 --port=2``.
+        """
+        return ParamsModifier(Params_multiple=True)
+
+    @staticmethod
+    def convert_value(*funcs: FnPtr) -> ParamsModifier:
+        """Takes in a variable number of functions to convert the value text representation.
+
+        Functions can be chained together, executed from left to right in the arguments list order.
+
+        Args:
+            *funcs: The functions to chain from left to right.
+
+        Returns:
+            ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
+                the convert value modifier.
+
+        Example:
+            .. code:: python
+
+                hex_bitmask: int | None = field(
+                    default=0b1101,
+                    metadata=Params.convert_value(hex) | Params.long("mask")
+                )
+
+            will render as ``--mask=0xd``.
+        """
+        return ParamsModifier(Params_convert_value=funcs)
+
+    """========= END FIELD METADATA MODIFIER FUNCTIONS ========"""
+
+    def append_str(self, text: str) -> None:
+        """Appends a string at the end of the string representation.
+
+        Args:
+            text: Any text to append at the end of the parameters string representation.
+        """
+        self._suffix += text
+
+    def __iadd__(self, text: str) -> Self:
+        """Appends a string at the end of the string representation.
+
+        Args:
+            text: Any text to append at the end of the parameters string representation.
+
+        Returns:
+            The given instance back.
+        """
+        self.append_str(text)
+        return self
+
+    @classmethod
+    def from_str(cls, text: str) -> Self:
+        """Creates a plain Params object from a string.
+
+        Args:
+            text: The string parameters.
+
+        Returns:
+            A new plain instance of :class:`Params`.
+        """
+        obj = cls()
+        obj.append_str(text)
+        return obj
+
+    @staticmethod
+    def _make_switch(
+        name: str, is_short: bool = False, is_no: bool = False, value: str | None = None
+    ) -> str:
+        """Make the string representation of the parameter.
+
+        Args:
+            name: The name of the parameters.
+            is_short: If the parameters is short or not.
+            is_no: If the parameter is negated or not.
+            value: The value of the parameter.
+
+        Returns:
+            The complete command line parameter.
+        """
+        prefix = f"{'-' if is_short else '--'}{'no-' if is_no else ''}"
+        name = name.replace("_", "-")
+        value = f"{' ' if is_short else '='}{value}" if value else ""
+        return f"{prefix}{name}{value}"
+
+    def __str__(self) -> str:
+        """Returns a string of command-line-ready arguments from the class fields."""
+        arguments: list[str] = []
+
+        for field in fields(self):
+            value = getattr(self, field.name)
+            modifiers = cast(ParamsModifier, field.metadata)
+
+            if value is None:
+                continue
+
+            if isinstance(value, Params):
+                arguments.append(str(value))
+                continue
+
+            # take the short modifier, or the long modifier, or infer from field name
+            switch_name = modifiers.get("Params_short", modifiers.get("Params_long", field.name))
+            is_short = "Params_short" in modifiers
+
+            if isinstance(value, bool):
+                arguments.append(self._make_switch(switch_name, is_short, is_no=(not value)))
+                continue
+
+            convert = _reduce_functions(modifiers.get("Params_convert_value", []))
+            multiple = modifiers.get("Params_multiple", False)
+
+            values = value if multiple else [value]
+            for value in values:
+                arguments.append(self._make_switch(switch_name, is_short, value=convert(value)))
+
+        if self._suffix:
+            arguments.append(self._suffix)
+
+        return " ".join(arguments)
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v6 2/8] dts: use Params for interactive shells
  2024-06-19 10:23 ` [PATCH v6 0/8] dts: add testpmd params Luca Vizzarro
  2024-06-19 10:23   ` [PATCH v6 1/8] dts: add params manipulation module Luca Vizzarro
@ 2024-06-19 10:23   ` Luca Vizzarro
  2024-06-19 10:23   ` [PATCH v6 3/8] dts: refactor EalParams Luca Vizzarro
                     ` (5 subsequent siblings)
  7 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-19 10:23 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Make it so that interactive shells accept an implementation of `Params`
for app arguments. Convert EalParameters to use `Params` instead.

String command line parameters can still be supplied by using the
`Params.from_str()` method.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 .../remote_session/interactive_shell.py       |  12 +-
 dts/framework/remote_session/testpmd_shell.py |  11 +-
 dts/framework/testbed_model/node.py           |   6 +-
 dts/framework/testbed_model/os_session.py     |   4 +-
 dts/framework/testbed_model/sut_node.py       | 124 ++++++++----------
 dts/tests/TestSuite_pmd_buffer_scatter.py     |   3 +-
 6 files changed, 77 insertions(+), 83 deletions(-)

diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
index c025c52ba3..8191b36630 100644
--- a/dts/framework/remote_session/interactive_shell.py
+++ b/dts/framework/remote_session/interactive_shell.py
@@ -1,5 +1,6 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright(c) 2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """Common functionality for interactive shell handling.
 
@@ -21,6 +22,7 @@
 from paramiko import Channel, SSHClient, channel  # type: ignore[import-untyped]
 
 from framework.logger import DTSLogger
+from framework.params import Params
 from framework.settings import SETTINGS
 
 
@@ -40,7 +42,7 @@ class InteractiveShell(ABC):
     _ssh_channel: Channel
     _logger: DTSLogger
     _timeout: float
-    _app_args: str
+    _app_params: Params
 
     #: Prompt to expect at the end of output when sending a command.
     #: This is often overridden by subclasses.
@@ -63,7 +65,7 @@ def __init__(
         interactive_session: SSHClient,
         logger: DTSLogger,
         get_privileged_command: Callable[[str], str] | None,
-        app_args: str = "",
+        app_params: Params = Params(),
         timeout: float = SETTINGS.timeout,
     ) -> None:
         """Create an SSH channel during initialization.
@@ -74,7 +76,7 @@ def __init__(
             get_privileged_command: A method for modifying a command to allow it to use
                 elevated privileges. If :data:`None`, the application will not be started
                 with elevated privileges.
-            app_args: The command line arguments to be passed to the application on startup.
+            app_params: The command line parameters to be passed to the application on startup.
             timeout: The timeout used for the SSH channel that is dedicated to this interactive
                 shell. This timeout is for collecting output, so if reading from the buffer
                 and no output is gathered within the timeout, an exception is thrown.
@@ -87,7 +89,7 @@ def __init__(
         self._ssh_channel.set_combine_stderr(True)  # combines stdout and stderr streams
         self._logger = logger
         self._timeout = timeout
-        self._app_args = app_args
+        self._app_params = app_params
         self._start_application(get_privileged_command)
 
     def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
@@ -100,7 +102,7 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
             get_privileged_command: A function (but could be any callable) that produces
                 the version of the command with elevated privileges.
         """
-        start_command = f"{self.path} {self._app_args}"
+        start_command = f"{self.path} {self._app_params}"
         if get_privileged_command is not None:
             start_command = get_privileged_command(start_command)
         self.send_command(start_command)
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index d413bf2cc7..2836ed5c48 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -28,6 +28,7 @@
 from framework.exception import InteractiveCommandExecutionError
 from framework.parser import ParserFn, TextParser
 from framework.settings import SETTINGS
+from framework.testbed_model.sut_node import EalParams
 from framework.utils import StrEnum
 
 from .interactive_shell import InteractiveShell
@@ -645,8 +646,14 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
         Also find the number of pci addresses which were allowed on the command line when the app
         was started.
         """
-        self._app_args += " -i --mask-event intr_lsc"
-        self.number_of_ports = self._app_args.count("-a ")
+        self._app_params += " -i --mask-event intr_lsc"
+
+        assert isinstance(self._app_params, EalParams)
+
+        self.number_of_ports = (
+            len(self._app_params.ports) if self._app_params.ports is not None else 0
+        )
+
         super()._start_application(get_privileged_command)
 
     def start(self, verify: bool = True) -> None:
diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
index 74061f6262..6af4f25a3c 100644
--- a/dts/framework/testbed_model/node.py
+++ b/dts/framework/testbed_model/node.py
@@ -2,6 +2,7 @@
 # Copyright(c) 2010-2014 Intel Corporation
 # Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2022-2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """Common functionality for node management.
 
@@ -24,6 +25,7 @@
 )
 from framework.exception import ConfigurationError
 from framework.logger import DTSLogger, get_dts_logger
+from framework.params import Params
 from framework.settings import SETTINGS
 
 from .cpu import (
@@ -199,7 +201,7 @@ def create_interactive_shell(
         shell_cls: Type[InteractiveShellType],
         timeout: float = SETTINGS.timeout,
         privileged: bool = False,
-        app_args: str = "",
+        app_params: Params = Params(),
     ) -> InteractiveShellType:
         """Factory for interactive session handlers.
 
@@ -222,7 +224,7 @@ def create_interactive_shell(
             shell_cls,
             timeout,
             privileged,
-            app_args,
+            app_params,
         )
 
     def filter_lcores(
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
index d5bf7e0401..1a77aee532 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright(c) 2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """OS-aware remote session.
 
@@ -29,6 +30,7 @@
 
 from framework.config import Architecture, NodeConfiguration, NodeInfo
 from framework.logger import DTSLogger
+from framework.params import Params
 from framework.remote_session import (
     CommandResult,
     InteractiveRemoteSession,
@@ -134,7 +136,7 @@ def create_interactive_shell(
         shell_cls: Type[InteractiveShellType],
         timeout: float,
         privileged: bool,
-        app_args: str,
+        app_args: Params,
     ) -> InteractiveShellType:
         """Factory for interactive session handlers.
 
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index 97aa26d419..c886590979 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -2,6 +2,7 @@
 # Copyright(c) 2010-2014 Intel Corporation
 # Copyright(c) 2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """System under test (DPDK + hardware) node.
 
@@ -14,8 +15,9 @@
 import os
 import tarfile
 import time
+from dataclasses import dataclass, field
 from pathlib import PurePath
-from typing import Type
+from typing import Literal, Type
 
 from framework.config import (
     BuildTargetConfiguration,
@@ -23,6 +25,7 @@
     NodeInfo,
     SutNodeConfiguration,
 )
+from framework.params import Params, Switch
 from framework.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
@@ -34,62 +37,42 @@
 from .virtual_device import VirtualDevice
 
 
-class EalParameters(object):
-    """The environment abstraction layer parameters.
-
-    The string representation can be created by converting the instance to a string.
-    """
+def _port_to_pci(port: Port) -> str:
+    return port.pci
 
-    def __init__(
-        self,
-        lcore_list: LogicalCoreList,
-        memory_channels: int,
-        prefix: str,
-        no_pci: bool,
-        vdevs: list[VirtualDevice],
-        ports: list[Port],
-        other_eal_param: str,
-    ):
-        """Initialize the parameters according to inputs.
-
-        Process the parameters into the format used on the command line.
 
-        Args:
-            lcore_list: The list of logical cores to use.
-            memory_channels: The number of memory channels to use.
-            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
-            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
-            vdevs: Virtual devices, e.g.::
+@dataclass(kw_only=True)
+class EalParams(Params):
+    """The environment abstraction layer parameters.
 
-                vdevs=[
-                    VirtualDevice('net_ring0'),
-                    VirtualDevice('net_ring1')
-                ]
-            ports: The list of ports to allow.
-            other_eal_param: user defined DPDK EAL parameters, e.g.:
+    Attributes:
+        lcore_list: The list of logical cores to use.
+        memory_channels: The number of memory channels to use.
+        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
+        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
+        vdevs: Virtual devices, e.g.::
+            vdevs=[
+                VirtualDevice('net_ring0'),
+                VirtualDevice('net_ring1')
+            ]
+        ports: The list of ports to allow.
+        other_eal_param: user defined DPDK EAL parameters, e.g.:
                 ``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._ports = " ".join(f"-a {port.pci}" for port in ports)
-        self._other_eal_param = other_eal_param
-
-    def __str__(self) -> str:
-        """Create the EAL string."""
-        return (
-            f"{self._lcore_list} "
-            f"{self._memory_channels} "
-            f"{self._prefix} "
-            f"{self._no_pci} "
-            f"{self._vdevs} "
-            f"{self._ports} "
-            f"{self._other_eal_param}"
-        )
+    """
+
+    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
+    memory_channels: int = field(metadata=Params.short("n"))
+    prefix: str = field(metadata=Params.long("file-prefix"))
+    no_pci: Switch
+    vdevs: list[VirtualDevice] | None = field(
+        default=None, metadata=Params.multiple() | Params.long("vdev")
+    )
+    ports: list[Port] | None = field(
+        default=None,
+        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
+    )
+    other_eal_param: Params | None = None
+    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
 
 
 class SutNode(Node):
@@ -350,11 +333,11 @@ def create_eal_parameters(
         ascending_cores: bool = True,
         prefix: str = "dpdk",
         append_prefix_timestamp: bool = True,
-        no_pci: bool = False,
+        no_pci: Switch = None,
         vdevs: list[VirtualDevice] | None = None,
         ports: list[Port] | None = None,
         other_eal_param: str = "",
-    ) -> "EalParameters":
+    ) -> EalParams:
         """Compose the EAL parameters.
 
         Process the list of cores and the DPDK prefix and pass that along with
@@ -393,24 +376,21 @@ def create_eal_parameters(
         if prefix:
             self._dpdk_prefix_list.append(prefix)
 
-        if vdevs is None:
-            vdevs = []
-
         if ports is None:
             ports = self.ports
 
-        return EalParameters(
+        return EalParams(
             lcore_list=lcore_list,
             memory_channels=self.config.memory_channels,
             prefix=prefix,
             no_pci=no_pci,
             vdevs=vdevs,
             ports=ports,
-            other_eal_param=other_eal_param,
+            other_eal_param=Params.from_str(other_eal_param),
         )
 
     def run_dpdk_app(
-        self, app_path: PurePath, eal_args: "EalParameters", timeout: float = 30
+        self, app_path: PurePath, eal_params: EalParams, timeout: float = 30
     ) -> CommandResult:
         """Run DPDK application on the remote node.
 
@@ -419,14 +399,14 @@ def run_dpdk_app(
 
         Args:
             app_path: The remote path to the DPDK application.
-            eal_args: EAL parameters to run the DPDK application with.
+            eal_params: EAL parameters to run the DPDK application with.
             timeout: Wait at most this long in seconds for `command` execution to complete.
 
         Returns:
             The result of the DPDK app execution.
         """
         return self.main_session.send_command(
-            f"{app_path} {eal_args}", timeout, privileged=True, verify=True
+            f"{app_path} {eal_params}", timeout, privileged=True, verify=True
         )
 
     def configure_ipv4_forwarding(self, enable: bool) -> None:
@@ -442,8 +422,8 @@ def create_interactive_shell(
         shell_cls: Type[InteractiveShellType],
         timeout: float = SETTINGS.timeout,
         privileged: bool = False,
-        app_parameters: str = "",
-        eal_parameters: EalParameters | None = None,
+        app_params: Params = Params(),
+        eal_params: EalParams | None = None,
     ) -> InteractiveShellType:
         """Extend the factory for interactive session handlers.
 
@@ -459,26 +439,26 @@ def create_interactive_shell(
                 reading from the buffer and don't receive any data within the timeout
                 it will throw an error.
             privileged: Whether to run the shell with administrative privileges.
-            eal_parameters: List of EAL parameters to use to launch the app. If this
+            app_params: The parameters to be passed to the application.
+            eal_params: List of EAL parameters to use to launch the app. If this
                 isn't provided or an empty string is passed, it will default to calling
                 :meth:`create_eal_parameters`.
-            app_parameters: Additional arguments to pass into the application on the
-                command-line.
 
         Returns:
             An instance of the desired interactive application shell.
         """
         # We need to append the build directory and add EAL parameters for DPDK apps
         if shell_cls.dpdk_app:
-            if not eal_parameters:
-                eal_parameters = self.create_eal_parameters()
-            app_parameters = f"{eal_parameters} -- {app_parameters}"
+            if eal_params is None:
+                eal_params = self.create_eal_parameters()
+            eal_params.append_str(str(app_params))
+            app_params = eal_params
 
             shell_cls.path = self.main_session.join_remote_path(
                 self.remote_dpdk_build_dir, shell_cls.path
             )
 
-        return super().create_interactive_shell(shell_cls, timeout, privileged, app_parameters)
+        return super().create_interactive_shell(shell_cls, timeout, privileged, app_params)
 
     def bind_ports_to_driver(self, for_dpdk: bool = True) -> None:
         """Bind all ports on the SUT to a driver.
diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index a020682e8d..c6e93839cb 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -22,6 +22,7 @@
 from scapy.packet import Raw  # type: ignore[import-untyped]
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
+from framework.params import Params
 from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
 from framework.test_suite import TestSuite
 
@@ -103,7 +104,7 @@ def pmd_scatter(self, mbsize: int) -> None:
         """
         testpmd = self.sut_node.create_interactive_shell(
             TestPmdShell,
-            app_parameters=(
+            app_params=Params.from_str(
                 "--mbcache=200 "
                 f"--mbuf-size={mbsize} "
                 "--max-pkt-len=9000 "
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v6 3/8] dts: refactor EalParams
  2024-06-19 10:23 ` [PATCH v6 0/8] dts: add testpmd params Luca Vizzarro
  2024-06-19 10:23   ` [PATCH v6 1/8] dts: add params manipulation module Luca Vizzarro
  2024-06-19 10:23   ` [PATCH v6 2/8] dts: use Params for interactive shells Luca Vizzarro
@ 2024-06-19 10:23   ` Luca Vizzarro
  2024-06-19 10:23   ` [PATCH v6 4/8] dts: remove module-wide imports Luca Vizzarro
                     ` (4 subsequent siblings)
  7 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-19 10:23 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Move EalParams to its own module to avoid circular dependencies.
Also the majority of the attributes are now optional.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 dts/framework/params/eal.py                   | 50 +++++++++++++++++++
 dts/framework/remote_session/testpmd_shell.py |  2 +-
 dts/framework/testbed_model/sut_node.py       | 42 +---------------
 3 files changed, 53 insertions(+), 41 deletions(-)
 create mode 100644 dts/framework/params/eal.py

diff --git a/dts/framework/params/eal.py b/dts/framework/params/eal.py
new file mode 100644
index 0000000000..bbdbc8f334
--- /dev/null
+++ b/dts/framework/params/eal.py
@@ -0,0 +1,50 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Module representing the DPDK EAL-related parameters."""
+
+from dataclasses import dataclass, field
+from typing import Literal
+
+from framework.params import Params, Switch
+from framework.testbed_model.cpu import LogicalCoreList
+from framework.testbed_model.port import Port
+from framework.testbed_model.virtual_device import VirtualDevice
+
+
+def _port_to_pci(port: Port) -> str:
+    return port.pci
+
+
+@dataclass(kw_only=True)
+class EalParams(Params):
+    """The environment abstraction layer parameters.
+
+    Attributes:
+        lcore_list: The list of logical cores to use.
+        memory_channels: The number of memory channels to use.
+        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
+        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
+        vdevs: Virtual devices, e.g.::
+            vdevs=[
+                VirtualDevice('net_ring0'),
+                VirtualDevice('net_ring1')
+            ]
+        ports: The list of ports to allow.
+        other_eal_param: user defined DPDK EAL parameters, e.g.:
+                ``other_eal_param='--single-file-segments'``
+    """
+
+    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
+    memory_channels: int = field(metadata=Params.short("n"))
+    prefix: str = field(metadata=Params.long("file-prefix"))
+    no_pci: Switch = None
+    vdevs: list[VirtualDevice] | None = field(
+        default=None, metadata=Params.multiple() | Params.long("vdev")
+    )
+    ports: list[Port] | None = field(
+        default=None,
+        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
+    )
+    other_eal_param: Params | None = None
+    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index 2836ed5c48..2b9ef9418d 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -26,9 +26,9 @@
 from typing_extensions import Self
 
 from framework.exception import InteractiveCommandExecutionError
+from framework.params.eal import EalParams
 from framework.parser import ParserFn, TextParser
 from framework.settings import SETTINGS
-from framework.testbed_model.sut_node import EalParams
 from framework.utils import StrEnum
 
 from .interactive_shell import InteractiveShell
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index c886590979..e1163106a3 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -15,9 +15,8 @@
 import os
 import tarfile
 import time
-from dataclasses import dataclass, field
 from pathlib import PurePath
-from typing import Literal, Type
+from typing import Type
 
 from framework.config import (
     BuildTargetConfiguration,
@@ -26,6 +25,7 @@
     SutNodeConfiguration,
 )
 from framework.params import Params, Switch
+from framework.params.eal import EalParams
 from framework.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
@@ -37,44 +37,6 @@
 from .virtual_device import VirtualDevice
 
 
-def _port_to_pci(port: Port) -> str:
-    return port.pci
-
-
-@dataclass(kw_only=True)
-class EalParams(Params):
-    """The environment abstraction layer parameters.
-
-    Attributes:
-        lcore_list: The list of logical cores to use.
-        memory_channels: The number of memory channels to use.
-        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
-        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
-        vdevs: Virtual devices, e.g.::
-            vdevs=[
-                VirtualDevice('net_ring0'),
-                VirtualDevice('net_ring1')
-            ]
-        ports: The list of ports to allow.
-        other_eal_param: user defined DPDK EAL parameters, e.g.:
-                ``other_eal_param='--single-file-segments'``
-    """
-
-    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
-    memory_channels: int = field(metadata=Params.short("n"))
-    prefix: str = field(metadata=Params.long("file-prefix"))
-    no_pci: Switch
-    vdevs: list[VirtualDevice] | None = field(
-        default=None, metadata=Params.multiple() | Params.long("vdev")
-    )
-    ports: list[Port] | None = field(
-        default=None,
-        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
-    )
-    other_eal_param: Params | None = None
-    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
-
-
 class SutNode(Node):
     """The system under test node.
 
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v6 4/8] dts: remove module-wide imports
  2024-06-19 10:23 ` [PATCH v6 0/8] dts: add testpmd params Luca Vizzarro
                     ` (2 preceding siblings ...)
  2024-06-19 10:23   ` [PATCH v6 3/8] dts: refactor EalParams Luca Vizzarro
@ 2024-06-19 10:23   ` Luca Vizzarro
  2024-06-19 10:23   ` [PATCH v6 5/8] dts: add testpmd shell params Luca Vizzarro
                     ` (3 subsequent siblings)
  7 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-19 10:23 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Remove the imports in the testbed_model and remote_session modules init
file, to avoid the initialisation of unneeded modules, thus removing or
limiting the risk of circular dependencies.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 dts/framework/remote_session/__init__.py                 | 7 +------
 dts/framework/runner.py                                  | 4 +++-
 dts/framework/test_suite.py                              | 9 +++++++--
 dts/framework/testbed_model/__init__.py                  | 9 ---------
 dts/framework/testbed_model/os_session.py                | 4 ++--
 dts/framework/testbed_model/sut_node.py                  | 2 +-
 dts/framework/testbed_model/tg_node.py                   | 9 ++++-----
 .../testbed_model/traffic_generator/__init__.py          | 7 +------
 dts/framework/testbed_model/traffic_generator/scapy.py   | 2 +-
 dts/tests/TestSuite_hello_world.py                       | 2 +-
 dts/tests/TestSuite_smoke_tests.py                       | 2 +-
 11 files changed, 22 insertions(+), 35 deletions(-)

diff --git a/dts/framework/remote_session/__init__.py b/dts/framework/remote_session/__init__.py
index 1910c81c3c..0668e9c884 100644
--- a/dts/framework/remote_session/__init__.py
+++ b/dts/framework/remote_session/__init__.py
@@ -12,17 +12,12 @@
 allowing it to send and receive data within that particular shell.
 """
 
-# pylama:ignore=W0611
-
 from framework.config import NodeConfiguration
 from framework.logger import DTSLogger
 
 from .interactive_remote_session import InteractiveRemoteSession
-from .interactive_shell import InteractiveShell
-from .python_shell import PythonShell
-from .remote_session import CommandResult, RemoteSession
+from .remote_session import RemoteSession
 from .ssh_session import SSHSession
-from .testpmd_shell import TestPmdShell
 
 
 def create_remote_session(
diff --git a/dts/framework/runner.py b/dts/framework/runner.py
index dfdee14802..687bc04f79 100644
--- a/dts/framework/runner.py
+++ b/dts/framework/runner.py
@@ -26,6 +26,9 @@
 from types import FunctionType
 from typing import Iterable, Sequence
 
+from framework.testbed_model.sut_node import SutNode
+from framework.testbed_model.tg_node import TGNode
+
 from .config import (
     BuildTargetConfiguration,
     Configuration,
@@ -51,7 +54,6 @@
     TestSuiteWithCases,
 )
 from .test_suite import TestSuite
-from .testbed_model import SutNode, TGNode
 
 
 class DTSRunner:
diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py
index 8768f756a6..91231fc8ef 100644
--- a/dts/framework/test_suite.py
+++ b/dts/framework/test_suite.py
@@ -20,10 +20,15 @@
 from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
 from scapy.packet import Packet, Padding  # type: ignore[import-untyped]
 
+from framework.testbed_model.port import Port, PortLink
+from framework.testbed_model.sut_node import SutNode
+from framework.testbed_model.tg_node import TGNode
+from framework.testbed_model.traffic_generator.capturing_traffic_generator import (
+    PacketFilteringConfig,
+)
+
 from .exception import TestCaseVerifyError
 from .logger import DTSLogger, get_dts_logger
-from .testbed_model import Port, PortLink, SutNode, TGNode
-from .testbed_model.traffic_generator import PacketFilteringConfig
 from .utils import get_packet_summaries
 
 
diff --git a/dts/framework/testbed_model/__init__.py b/dts/framework/testbed_model/__init__.py
index 6086512ca2..e3edd4d811 100644
--- a/dts/framework/testbed_model/__init__.py
+++ b/dts/framework/testbed_model/__init__.py
@@ -17,12 +17,3 @@
 DTS needs to be able to connect to nodes and understand some of the hardware present on these nodes
 to properly build and test DPDK.
 """
-
-# pylama:ignore=W0611
-
-from .cpu import LogicalCoreCount, LogicalCoreCountFilter, LogicalCoreList
-from .node import Node
-from .port import Port, PortLink
-from .sut_node import SutNode
-from .tg_node import TGNode
-from .virtual_device import VirtualDevice
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
index 1a77aee532..e5f5fcbe0e 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -32,13 +32,13 @@
 from framework.logger import DTSLogger
 from framework.params import Params
 from framework.remote_session import (
-    CommandResult,
     InteractiveRemoteSession,
-    InteractiveShell,
     RemoteSession,
     create_interactive_session,
     create_remote_session,
 )
+from framework.remote_session.interactive_shell import InteractiveShell
+from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
 
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index e1163106a3..83ad06ae2d 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -26,7 +26,7 @@
 )
 from framework.params import Params, Switch
 from framework.params.eal import EalParams
-from framework.remote_session import CommandResult
+from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
 
diff --git a/dts/framework/testbed_model/tg_node.py b/dts/framework/testbed_model/tg_node.py
index 164f790383..d1c86c5738 100644
--- a/dts/framework/testbed_model/tg_node.py
+++ b/dts/framework/testbed_model/tg_node.py
@@ -12,14 +12,13 @@
 from scapy.packet import Packet  # type: ignore[import-untyped]
 
 from framework.config import TGNodeConfiguration
+from framework.testbed_model.traffic_generator.capturing_traffic_generator import (
+    PacketFilteringConfig,
+)
 
 from .node import Node
 from .port import Port
-from .traffic_generator import (
-    CapturingTrafficGenerator,
-    PacketFilteringConfig,
-    create_traffic_generator,
-)
+from .traffic_generator import CapturingTrafficGenerator, create_traffic_generator
 
 
 class TGNode(Node):
diff --git a/dts/framework/testbed_model/traffic_generator/__init__.py b/dts/framework/testbed_model/traffic_generator/__init__.py
index 03e57a77fc..6dac86a224 100644
--- a/dts/framework/testbed_model/traffic_generator/__init__.py
+++ b/dts/framework/testbed_model/traffic_generator/__init__.py
@@ -14,16 +14,11 @@
 and a capturing traffic generator is required.
 """
 
-# pylama:ignore=W0611
-
 from framework.config import ScapyTrafficGeneratorConfig, TrafficGeneratorConfig
 from framework.exception import ConfigurationError
 from framework.testbed_model.node import Node
 
-from .capturing_traffic_generator import (
-    CapturingTrafficGenerator,
-    PacketFilteringConfig,
-)
+from .capturing_traffic_generator import CapturingTrafficGenerator
 from .scapy import ScapyTrafficGenerator
 
 
diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py
index ed5467d825..7bc1c2cc08 100644
--- a/dts/framework/testbed_model/traffic_generator/scapy.py
+++ b/dts/framework/testbed_model/traffic_generator/scapy.py
@@ -25,7 +25,7 @@
 from scapy.packet import Packet  # type: ignore[import-untyped]
 
 from framework.config import OS, ScapyTrafficGeneratorConfig
-from framework.remote_session import PythonShell
+from framework.remote_session.python_shell import PythonShell
 from framework.settings import SETTINGS
 from framework.testbed_model.node import Node
 from framework.testbed_model.port import Port
diff --git a/dts/tests/TestSuite_hello_world.py b/dts/tests/TestSuite_hello_world.py
index fd7ff1534d..0d6995f260 100644
--- a/dts/tests/TestSuite_hello_world.py
+++ b/dts/tests/TestSuite_hello_world.py
@@ -8,7 +8,7 @@
 """
 
 from framework.test_suite import TestSuite
-from framework.testbed_model import (
+from framework.testbed_model.cpu import (
     LogicalCoreCount,
     LogicalCoreCountFilter,
     LogicalCoreList,
diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py
index a553e89662..ca678f662d 100644
--- a/dts/tests/TestSuite_smoke_tests.py
+++ b/dts/tests/TestSuite_smoke_tests.py
@@ -15,7 +15,7 @@
 import re
 
 from framework.config import PortConfig
-from framework.remote_session import TestPmdShell
+from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.settings import SETTINGS
 from framework.test_suite import TestSuite
 from framework.utils import REGEX_FOR_PCI_ADDRESS
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v6 5/8] dts: add testpmd shell params
  2024-06-19 10:23 ` [PATCH v6 0/8] dts: add testpmd params Luca Vizzarro
                     ` (3 preceding siblings ...)
  2024-06-19 10:23   ` [PATCH v6 4/8] dts: remove module-wide imports Luca Vizzarro
@ 2024-06-19 10:23   ` Luca Vizzarro
  2024-06-19 10:23   ` [PATCH v6 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
                     ` (2 subsequent siblings)
  7 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-19 10:23 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Implement all the testpmd shell parameters into a data structure.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 dts/framework/params/testpmd.py               | 607 ++++++++++++++++++
 dts/framework/remote_session/testpmd_shell.py |  39 +-
 dts/tests/TestSuite_pmd_buffer_scatter.py     |   5 +-
 3 files changed, 613 insertions(+), 38 deletions(-)
 create mode 100644 dts/framework/params/testpmd.py

diff --git a/dts/framework/params/testpmd.py b/dts/framework/params/testpmd.py
new file mode 100644
index 0000000000..1913bd0fa2
--- /dev/null
+++ b/dts/framework/params/testpmd.py
@@ -0,0 +1,607 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Module containing all the TestPmd-related parameter classes."""
+
+from dataclasses import dataclass, field
+from enum import EnumMeta, Flag, auto, unique
+from pathlib import PurePath
+from typing import Literal, NamedTuple
+
+from framework.params import (
+    Params,
+    Switch,
+    YesNoSwitch,
+    bracketed,
+    comma_separated,
+    hex_from_flag_value,
+    modify_str,
+    str_from_flag_value,
+)
+from framework.params.eal import EalParams
+from framework.utils import StrEnum
+
+
+class PortTopology(StrEnum):
+    """Enum representing the port topology."""
+
+    #: In paired mode, the forwarding is between pairs of ports, e.g.: (0,1), (2,3), (4,5).
+    paired = auto()
+
+    #: In chained mode, the forwarding is to the next available port in the port mask, e.g.:
+    #: (0,1), (1,2), (2,0).
+    #:
+    #: The ordering of the ports can be changed using the portlist testpmd runtime function.
+    chained = auto()
+
+    #: In loop mode, ingress traffic is simply transmitted back on the same interface.
+    loop = auto()
+
+
+@modify_str(comma_separated, bracketed)
+class PortNUMAConfig(NamedTuple):
+    """DPDK port to NUMA socket association tuple."""
+
+    #:
+    port: int
+    #:
+    socket: int
+
+
+@modify_str(str_from_flag_value)
+@unique
+class FlowDirection(Flag):
+    """Flag indicating the direction of the flow.
+
+    A bi-directional flow can be specified with the pipe:
+
+    >>> TestPmdFlowDirection.RX | TestPmdFlowDirection.TX
+    <TestPmdFlowDirection.TX|RX: 3>
+    """
+
+    #:
+    RX = 1 << 0
+    #:
+    TX = 1 << 1
+
+
+@modify_str(comma_separated, bracketed)
+class RingNUMAConfig(NamedTuple):
+    """Tuple associating DPDK port, direction of the flow and NUMA socket."""
+
+    #:
+    port: int
+    #:
+    direction: FlowDirection
+    #:
+    socket: int
+
+
+@modify_str(comma_separated)
+class EthPeer(NamedTuple):
+    """Tuple associating a MAC address to the specified DPDK port."""
+
+    #:
+    port_no: int
+    #:
+    mac_address: str
+
+
+@modify_str(comma_separated)
+class TxIPAddrPair(NamedTuple):
+    """Tuple specifying the source and destination IPs for the packets."""
+
+    #:
+    source_ip: str
+    #:
+    dest_ip: str
+
+
+@modify_str(comma_separated)
+class TxUDPPortPair(NamedTuple):
+    """Tuple specifying the UDP source and destination ports for the packets.
+
+    If leaving ``dest_port`` unspecified, ``source_port`` will be used for
+    the destination port as well.
+    """
+
+    #:
+    source_port: int
+    #:
+    dest_port: int | None = None
+
+
+@dataclass
+class DisableRSS(Params):
+    """Disables RSS (Receive Side Scaling)."""
+
+    _disable_rss: Literal[True] = field(
+        default=True, init=False, metadata=Params.long("disable-rss")
+    )
+
+
+@dataclass
+class SetRSSIPOnly(Params):
+    """Sets RSS (Receive Side Scaling) functions for IPv4/IPv6 only."""
+
+    _rss_ip: Literal[True] = field(default=True, init=False, metadata=Params.long("rss-ip"))
+
+
+@dataclass
+class SetRSSUDP(Params):
+    """Sets RSS (Receive Side Scaling) functions for IPv4/IPv6 and UDP."""
+
+    _rss_udp: Literal[True] = field(default=True, init=False, metadata=Params.long("rss-udp"))
+
+
+class RSSSetting(EnumMeta):
+    """Enum representing a RSS setting. Each property is a class that needs to be initialised."""
+
+    #:
+    Disabled = DisableRSS
+    #:
+    SetIPOnly = SetRSSIPOnly
+    #:
+    SetUDP = SetRSSUDP
+
+
+class SimpleForwardingModes(StrEnum):
+    r"""The supported packet forwarding modes for :class:`~TestPmdShell`\s."""
+
+    #:
+    io = auto()
+    #:
+    mac = auto()
+    #:
+    macswap = auto()
+    #:
+    rxonly = auto()
+    #:
+    csum = auto()
+    #:
+    icmpecho = auto()
+    #:
+    ieee1588 = auto()
+    #:
+    fivetswap = "5tswap"
+    #:
+    shared_rxq = "shared-rxq"
+    #:
+    recycle_mbufs = auto()
+
+
+@dataclass(kw_only=True)
+class TXOnlyForwardingMode(Params):
+    """Sets a TX-Only forwarding mode.
+
+    Attributes:
+        multi_flow: Generates multiple flows if set to True.
+        segments_length: Sets TX segment sizes or total packet length.
+    """
+
+    _forward_mode: Literal["txonly"] = field(
+        default="txonly", init=False, metadata=Params.long("forward-mode")
+    )
+    multi_flow: Switch = field(default=None, metadata=Params.long("txonly-multi-flow"))
+    segments_length: list[int] | None = field(
+        default=None, metadata=Params.long("txpkts") | Params.convert_value(comma_separated)
+    )
+
+
+@dataclass(kw_only=True)
+class FlowGenForwardingMode(Params):
+    """Sets a flowgen forwarding mode.
+
+    Attributes:
+        clones: Set the number of each packet clones to be sent. Sending clones reduces host CPU
+                load on creating packets and may help in testing extreme speeds or maxing out
+                Tx packet performance. N should be not zero, but less than ‘burst’ parameter.
+        flows: Set the number of flows to be generated, where 1 <= N <= INT32_MAX.
+        segments_length: Set TX segment sizes or total packet length.
+    """
+
+    _forward_mode: Literal["flowgen"] = field(
+        default="flowgen", init=False, metadata=Params.long("forward-mode")
+    )
+    clones: int | None = field(default=None, metadata=Params.long("flowgen-clones"))
+    flows: int | None = field(default=None, metadata=Params.long("flowgen-flows"))
+    segments_length: list[int] | None = field(
+        default=None, metadata=Params.long("txpkts") | Params.convert_value(comma_separated)
+    )
+
+
+@dataclass(kw_only=True)
+class NoisyForwardingMode(Params):
+    """Sets a noisy forwarding mode.
+
+    Attributes:
+        forward_mode: Set the noisy VNF forwarding mode.
+        tx_sw_buffer_size: Set the maximum number of elements of the FIFO queue to be created for
+                           buffering packets.
+        tx_sw_buffer_flushtime: Set the time before packets in the FIFO queue are flushed.
+        lkup_memory: Set the size of the noisy neighbor simulation memory buffer in MB to N.
+        lkup_num_reads: Set the size of the noisy neighbor simulation memory buffer in MB to N.
+        lkup_num_writes: Set the number of writes to be done in noisy neighbor simulation
+                         memory buffer to N.
+        lkup_num_reads_writes: Set the number of r/w accesses to be done in noisy neighbor
+                               simulation memory buffer to N.
+    """
+
+    _forward_mode: Literal["noisy"] = field(
+        default="noisy", init=False, metadata=Params.long("forward-mode")
+    )
+    forward_mode: (
+        Literal[
+            SimpleForwardingModes.io,
+            SimpleForwardingModes.mac,
+            SimpleForwardingModes.macswap,
+            SimpleForwardingModes.fivetswap,
+        ]
+        | None
+    ) = field(default=SimpleForwardingModes.io, metadata=Params.long("noisy-forward-mode"))
+    tx_sw_buffer_size: int | None = field(
+        default=None, metadata=Params.long("noisy-tx-sw-buffer-size")
+    )
+    tx_sw_buffer_flushtime: int | None = field(
+        default=None, metadata=Params.long("noisy-tx-sw-buffer-flushtime")
+    )
+    lkup_memory: int | None = field(default=None, metadata=Params.long("noisy-lkup-memory"))
+    lkup_num_reads: int | None = field(default=None, metadata=Params.long("noisy-lkup-num-reads"))
+    lkup_num_writes: int | None = field(default=None, metadata=Params.long("noisy-lkup-num-writes"))
+    lkup_num_reads_writes: int | None = field(
+        default=None, metadata=Params.long("noisy-lkup-num-reads-writes")
+    )
+
+
+@modify_str(hex_from_flag_value)
+@unique
+class HairpinMode(Flag):
+    """Flag representing the hairpin mode."""
+
+    #: Two hairpin ports loop.
+    TWO_PORTS_LOOP = 1 << 0
+    #: Two hairpin ports paired.
+    TWO_PORTS_PAIRED = 1 << 1
+    #: Explicit Tx flow rule.
+    EXPLICIT_TX_FLOW = 1 << 4
+    #: Force memory settings of hairpin RX queue.
+    FORCE_RX_QUEUE_MEM_SETTINGS = 1 << 8
+    #: Force memory settings of hairpin TX queue.
+    FORCE_TX_QUEUE_MEM_SETTINGS = 1 << 9
+    #: Hairpin RX queues will use locked device memory.
+    RX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 12
+    #: Hairpin RX queues will use RTE memory.
+    RX_QUEUE_USE_RTE_MEMORY = 1 << 13
+    #: Hairpin TX queues will use locked device memory.
+    TX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 16
+    #: Hairpin TX queues will use RTE memory.
+    TX_QUEUE_USE_RTE_MEMORY = 1 << 18
+
+
+@dataclass(kw_only=True)
+class RXRingParams(Params):
+    """Sets the RX ring parameters.
+
+    Attributes:
+        descriptors: Set the number of descriptors in the RX rings to N, where N > 0.
+        prefetch_threshold: Set the prefetch threshold register of RX rings to N, where N >= 0.
+        host_threshold: Set the host threshold register of RX rings to N, where N >= 0.
+        write_back_threshold: Set the write-back threshold register of RX rings to N, where N >= 0.
+        free_threshold: Set the free threshold of RX descriptors to N,
+                        where 0 <= N < value of ``-–rxd``.
+    """
+
+    descriptors: int | None = field(default=None, metadata=Params.long("rxd"))
+    prefetch_threshold: int | None = field(default=None, metadata=Params.long("rxpt"))
+    host_threshold: int | None = field(default=None, metadata=Params.long("rxht"))
+    write_back_threshold: int | None = field(default=None, metadata=Params.long("rxwt"))
+    free_threshold: int | None = field(default=None, metadata=Params.long("rxfreet"))
+
+
+@modify_str(hex_from_flag_value)
+@unique
+class RXMultiQueueMode(Flag):
+    """Flag representing the RX multi-queue mode."""
+
+    #:
+    RSS = 1 << 0
+    #:
+    DCB = 1 << 1
+    #:
+    VMDQ = 1 << 2
+
+
+@dataclass(kw_only=True)
+class TXRingParams(Params):
+    """Sets the TX ring parameters.
+
+    Attributes:
+        descriptors: Set the number of descriptors in the TX rings to N, where N > 0.
+        rs_bit_threshold: Set the transmit RS bit threshold of TX rings to N,
+                          where 0 <= N <= value of ``--txd``.
+        prefetch_threshold: Set the prefetch threshold register of TX rings to N, where N >= 0.
+        host_threshold: Set the host threshold register of TX rings to N, where N >= 0.
+        write_back_threshold: Set the write-back threshold register of TX rings to N, where N >= 0.
+        free_threshold: Set the transmit free threshold of TX rings to N,
+                        where 0 <= N <= value of ``--txd``.
+    """
+
+    descriptors: int | None = field(default=None, metadata=Params.long("txd"))
+    rs_bit_threshold: int | None = field(default=None, metadata=Params.long("txrst"))
+    prefetch_threshold: int | None = field(default=None, metadata=Params.long("txpt"))
+    host_threshold: int | None = field(default=None, metadata=Params.long("txht"))
+    write_back_threshold: int | None = field(default=None, metadata=Params.long("txwt"))
+    free_threshold: int | None = field(default=None, metadata=Params.long("txfreet"))
+
+
+class Event(StrEnum):
+    """Enum representing a testpmd event."""
+
+    #:
+    unknown = auto()
+    #:
+    queue_state = auto()
+    #:
+    vf_mbox = auto()
+    #:
+    macsec = auto()
+    #:
+    intr_lsc = auto()
+    #:
+    intr_rmv = auto()
+    #:
+    intr_reset = auto()
+    #:
+    dev_probed = auto()
+    #:
+    dev_released = auto()
+    #:
+    flow_aged = auto()
+    #:
+    err_recovering = auto()
+    #:
+    recovery_success = auto()
+    #:
+    recovery_failed = auto()
+    #:
+    all = auto()
+
+
+class SimpleMempoolAllocationMode(StrEnum):
+    """Enum representing simple mempool allocation modes."""
+
+    #: Create and populate mempool using native DPDK memory.
+    native = auto()
+    #: Create and populate mempool using externally and anonymously allocated area.
+    xmem = auto()
+    #: Create and populate mempool using externally and anonymously allocated hugepage area.
+    xmemhuge = auto()
+
+
+@dataclass(kw_only=True)
+class AnonMempoolAllocationMode(Params):
+    """Create mempool using native DPDK memory, but populate using anonymous memory.
+
+    Attributes:
+        no_iova_contig: Enables to create mempool which is not IOVA contiguous.
+    """
+
+    _mp_alloc: Literal["anon"] = field(default="anon", init=False, metadata=Params.long("mp-alloc"))
+    no_iova_contig: Switch = None
+
+
+@dataclass(slots=True, kw_only=True)
+class TestPmdParams(EalParams):
+    """The testpmd shell parameters.
+
+    Attributes:
+        interactive_mode: Run testpmd in interactive mode.
+        auto_start: Start forwarding on initialization.
+        tx_first: Start forwarding, after sending a burst of packets first.
+        stats_period: Display statistics every ``PERIOD`` seconds, if interactive mode is disabled.
+                      The default value is 0, which means that the statistics will not be displayed.
+
+                      .. note:: This flag should be used only in non-interactive mode.
+        display_xstats: Display comma-separated list of extended statistics every ``PERIOD`` seconds
+                        as specified in ``--stats-period`` or when used with interactive commands
+                        that show Rx/Tx statistics (i.e. ‘show port stats’).
+        nb_cores: Set the number of forwarding cores, where 1 <= N <= “number of cores” or
+                  ``RTE_MAX_LCORE`` from the configuration file.
+        coremask: Set the bitmask of the cores running the packet forwarding test. The main
+                  lcore is reserved for command line parsing only and cannot be masked on for packet
+                  forwarding.
+        nb_ports: Set the number of forwarding ports, where 1 <= N <= “number of ports” on the board
+                  or ``RTE_MAX_ETHPORTS`` from the configuration file. The default value is the
+                  number of ports on the board.
+        port_topology: Set port topology, where mode is paired (the default), chained or loop.
+        portmask: Set the bitmask of the ports used by the packet forwarding test.
+        portlist: Set the forwarding ports based on the user input used by the packet forwarding
+                  test. ‘-‘ denotes a range of ports to set including the two specified port IDs ‘,’
+                  separates multiple port values. Possible examples like –portlist=0,1 or
+                  –portlist=0-2 or –portlist=0,1-2 etc.
+        numa: Enable/disable NUMA-aware allocation of RX/TX rings and of RX memory buffers (mbufs).
+        socket_num: Set the socket from which all memory is allocated in NUMA mode, where
+                    0 <= N < number of sockets on the board.
+        port_numa_config: Specify the socket on which the memory pool to be used by the port will be
+                          allocated.
+        ring_numa_config: Specify the socket on which the TX/RX rings for the port will be
+                          allocated. Where flag is 1 for RX, 2 for TX, and 3 for RX and TX.
+        total_num_mbufs: Set the number of mbufs to be allocated in the mbuf pools, where N > 1024.
+        mbuf_size: Set the data size of the mbufs used to N bytes, where N < 65536.
+                   If multiple mbuf-size values are specified the extra memory pools will be created
+                   for allocating mbufs to receive packets with buffer splitting features.
+        mbcache: Set the cache of mbuf memory pools to N, where 0 <= N <= 512.
+        max_pkt_len: Set the maximum packet size to N bytes, where N >= 64.
+        eth_peers_configfile: Use a configuration file containing the Ethernet addresses of
+                              the peer ports.
+        eth_peer: Set the MAC address XX:XX:XX:XX:XX:XX of the peer port N,
+                  where 0 <= N < RTE_MAX_ETHPORTS.
+        tx_ip: Set the source and destination IP address used when doing transmit only test.
+               The defaults address values are source 198.18.0.1 and destination 198.18.0.2.
+               These are special purpose addresses reserved for benchmarking (RFC 5735).
+        tx_udp: Set the source and destination UDP port number for transmit test only test.
+                The default port is the port 9 which is defined for the discard protocol (RFC 863).
+        enable_lro: Enable large receive offload.
+        max_lro_pkt_size: Set the maximum LRO aggregated packet size to N bytes, where N >= 64.
+        disable_crc_strip: Disable hardware CRC stripping.
+        enable_scatter: Enable scatter (multi-segment) RX.
+        enable_hw_vlan: Enable hardware VLAN.
+        enable_hw_vlan_filter: Enable hardware VLAN filter.
+        enable_hw_vlan_strip: Enable hardware VLAN strip.
+        enable_hw_vlan_extend: Enable hardware VLAN extend.
+        enable_hw_qinq_strip: Enable hardware QINQ strip.
+        pkt_drop_enabled: Enable per-queue packet drop for packets with no descriptors.
+        rss: Receive Side Scaling setting.
+        forward_mode: Set the forwarding mode.
+        hairpin_mode: Set the hairpin port configuration.
+        hairpin_queues: Set the number of hairpin queues per port to N, where 1 <= N <= 65535.
+        burst: Set the number of packets per burst to N, where 1 <= N <= 512.
+        enable_rx_cksum: Enable hardware RX checksum offload.
+        rx_queues: Set the number of RX queues per port to N, where 1 <= N <= 65535.
+        rx_ring: Set the RX rings parameters.
+        no_flush_rx: Don’t flush the RX streams before starting forwarding. Used mainly with
+                     the PCAP PMD.
+        rx_segments_offsets: Set the offsets of packet segments on receiving
+                             if split feature is engaged.
+        rx_segments_length: Set the length of segments to scatter packets on receiving
+                            if split feature is engaged.
+        multi_rx_mempool: Enable multiple mbuf pools per Rx queue.
+        rx_shared_queue: Create queues in shared Rx queue mode if device supports. Shared Rx queues
+                         are grouped per X ports. X defaults to UINT32_MAX, implies all ports join
+                         share group 1. Forwarding engine “shared-rxq” should be used for shared Rx
+                         queues. This engine does Rx only and update stream statistics accordingly.
+        rx_offloads: Set the bitmask of RX queue offloads.
+        rx_mq_mode: Set the RX multi queue mode which can be enabled.
+        tx_queues: Set the number of TX queues per port to N, where 1 <= N <= 65535.
+        tx_ring: Set the TX rings params.
+        tx_offloads: Set the hexadecimal bitmask of TX queue offloads.
+        eth_link_speed: Set a forced link speed to the ethernet port. E.g. 1000 for 1Gbps.
+        disable_link_check: Disable check on link status when starting/stopping ports.
+        disable_device_start: Do not automatically start all ports. This allows testing
+                              configuration of rx and tx queues before device is started
+                              for the first time.
+        no_lsc_interrupt: Disable LSC interrupts for all ports, even those supporting it.
+        no_rmv_interrupt: Disable RMV interrupts for all ports, even those supporting it.
+        bitrate_stats: Set the logical core N to perform bitrate calculation.
+        latencystats: Set the logical core N to perform latency and jitter calculations.
+        print_events: Enable printing the occurrence of the designated events.
+                      Using :attr:`TestPmdEvent.ALL` will enable all of them.
+        mask_events: Disable printing the occurrence of the designated events.
+                     Using :attr:`TestPmdEvent.ALL` will disable all of them.
+        flow_isolate_all: Providing this parameter requests flow API isolated mode on all ports at
+                          initialization time. It ensures all traffic is received through the
+                          configured flow rules only (see flow command). Ports that do not support
+                          this mode are automatically discarded.
+        disable_flow_flush: Disable port flow flush when stopping port.
+                            This allows testing keep flow rules or shared flow objects across
+                            restart.
+        hot_plug: Enable device event monitor mechanism for hotplug.
+        vxlan_gpe_port: Set the UDP port number of tunnel VXLAN-GPE to N.
+        geneve_parsed_port: Set the UDP port number that is used for parsing the GENEVE protocol
+                            to N. HW may be configured with another tunnel Geneve port.
+        lock_all_memory: Enable/disable locking all memory. Disabled by default.
+        mempool_allocation_mode: Set mempool allocation mode.
+        record_core_cycles: Enable measurement of CPU cycles per packet.
+        record_burst_status: Enable display of RX and TX burst stats.
+    """
+
+    interactive_mode: Switch = field(default=True, metadata=Params.short("i"))
+    auto_start: Switch = field(default=None, metadata=Params.short("a"))
+    tx_first: Switch = None
+    stats_period: int | None = None
+    display_xstats: list[str] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    nb_cores: int | None = None
+    coremask: int | None = field(default=None, metadata=Params.convert_value(hex))
+    nb_ports: int | None = None
+    port_topology: PortTopology | None = PortTopology.paired
+    portmask: int | None = field(default=None, metadata=Params.convert_value(hex))
+    portlist: str | None = None  # TODO: can be ranges 0,1-3
+
+    numa: YesNoSwitch = None
+    socket_num: int | None = None
+    port_numa_config: list[PortNUMAConfig] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    ring_numa_config: list[RingNUMAConfig] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    total_num_mbufs: int | None = None
+    mbuf_size: list[int] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    mbcache: int | None = None
+    max_pkt_len: int | None = None
+    eth_peers_configfile: PurePath | None = None
+    eth_peer: list[EthPeer] | None = field(default=None, metadata=Params.multiple())
+    tx_ip: TxIPAddrPair | None = None
+    tx_udp: TxUDPPortPair | None = None
+    enable_lro: Switch = None
+    max_lro_pkt_size: int | None = None
+    disable_crc_strip: Switch = None
+    enable_scatter: Switch = None
+    enable_hw_vlan: Switch = None
+    enable_hw_vlan_filter: Switch = None
+    enable_hw_vlan_strip: Switch = None
+    enable_hw_vlan_extend: Switch = None
+    enable_hw_qinq_strip: Switch = None
+    pkt_drop_enabled: Switch = field(default=None, metadata=Params.long("enable-drop-en"))
+    rss: RSSSetting | None = None
+    forward_mode: (
+        SimpleForwardingModes
+        | FlowGenForwardingMode
+        | TXOnlyForwardingMode
+        | NoisyForwardingMode
+        | None
+    ) = None
+    hairpin_mode: HairpinMode | None = None
+    hairpin_queues: int | None = field(default=None, metadata=Params.long("hairpinq"))
+    burst: int | None = None
+    enable_rx_cksum: Switch = None
+
+    rx_queues: int | None = field(default=None, metadata=Params.long("rxq"))
+    rx_ring: RXRingParams | None = None
+    no_flush_rx: Switch = None
+    rx_segments_offsets: list[int] | None = field(
+        default=None, metadata=Params.long("rxoffs") | Params.convert_value(comma_separated)
+    )
+    rx_segments_length: list[int] | None = field(
+        default=None, metadata=Params.long("rxpkts") | Params.convert_value(comma_separated)
+    )
+    multi_rx_mempool: Switch = None
+    rx_shared_queue: Switch | int = field(default=None, metadata=Params.long("rxq-share"))
+    rx_offloads: int | None = field(default=None, metadata=Params.convert_value(hex))
+    rx_mq_mode: RXMultiQueueMode | None = None
+
+    tx_queues: int | None = field(default=None, metadata=Params.long("txq"))
+    tx_ring: TXRingParams | None = None
+    tx_offloads: int | None = field(default=None, metadata=Params.convert_value(hex))
+
+    eth_link_speed: int | None = None
+    disable_link_check: Switch = None
+    disable_device_start: Switch = None
+    no_lsc_interrupt: Switch = None
+    no_rmv_interrupt: Switch = None
+    bitrate_stats: int | None = None
+    latencystats: int | None = None
+    print_events: list[Event] | None = field(
+        default=None, metadata=Params.multiple() | Params.long("print-event")
+    )
+    mask_events: list[Event] | None = field(
+        default_factory=lambda: [Event.intr_lsc],
+        metadata=Params.multiple() | Params.long("mask-event"),
+    )
+
+    flow_isolate_all: Switch = None
+    disable_flow_flush: Switch = None
+
+    hot_plug: Switch = None
+    vxlan_gpe_port: int | None = None
+    geneve_parsed_port: int | None = None
+    lock_all_memory: YesNoSwitch = field(default=None, metadata=Params.long("mlockall"))
+    mempool_allocation_mode: SimpleMempoolAllocationMode | AnonMempoolAllocationMode | None = field(
+        default=None, metadata=Params.long("mp-alloc")
+    )
+    record_core_cycles: Switch = None
+    record_burst_status: Switch = None
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index 2b9ef9418d..82701a9839 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -26,7 +26,7 @@
 from typing_extensions import Self
 
 from framework.exception import InteractiveCommandExecutionError
-from framework.params.eal import EalParams
+from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
 from framework.parser import ParserFn, TextParser
 from framework.settings import SETTINGS
 from framework.utils import StrEnum
@@ -56,37 +56,6 @@ def __str__(self) -> str:
         return self.pci_address
 
 
-class TestPmdForwardingModes(StrEnum):
-    r"""The supported packet forwarding modes for :class:`~TestPmdShell`\s."""
-
-    #:
-    io = auto()
-    #:
-    mac = auto()
-    #:
-    macswap = auto()
-    #:
-    flowgen = auto()
-    #:
-    rxonly = auto()
-    #:
-    txonly = auto()
-    #:
-    csum = auto()
-    #:
-    icmpecho = auto()
-    #:
-    ieee1588 = auto()
-    #:
-    noisy = auto()
-    #:
-    fivetswap = "5tswap"
-    #:
-    shared_rxq = "shared-rxq"
-    #:
-    recycle_mbufs = auto()
-
-
 class VLANOffloadFlag(Flag):
     """Flag representing the VLAN offload settings of a NIC port."""
 
@@ -646,9 +615,7 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
         Also find the number of pci addresses which were allowed on the command line when the app
         was started.
         """
-        self._app_params += " -i --mask-event intr_lsc"
-
-        assert isinstance(self._app_params, EalParams)
+        assert isinstance(self._app_params, TestPmdParams)
 
         self.number_of_ports = (
             len(self._app_params.ports) if self._app_params.ports is not None else 0
@@ -740,7 +707,7 @@ def wait_link_status_up(self, port_id: int, timeout=SETTINGS.timeout) -> bool:
             self._logger.error(f"The link for port {port_id} did not come up in the given timeout.")
         return "Link status: up" in port_info
 
-    def set_forward_mode(self, mode: TestPmdForwardingModes, verify: bool = True):
+    def set_forward_mode(self, mode: SimpleForwardingModes, verify: bool = True):
         """Set packet forwarding mode.
 
         Args:
diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index c6e93839cb..578b5a4318 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -23,7 +23,8 @@
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
 from framework.params import Params
-from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
+from framework.params.testpmd import SimpleForwardingModes
+from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.test_suite import TestSuite
 
 
@@ -113,7 +114,7 @@ def pmd_scatter(self, mbsize: int) -> None:
             ),
             privileged=True,
         )
-        testpmd.set_forward_mode(TestPmdForwardingModes.mac)
+        testpmd.set_forward_mode(SimpleForwardingModes.mac)
         testpmd.start()
 
         for offset in [-1, 0, 1, 4, 5]:
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v6 6/8] dts: use testpmd params for scatter test suite
  2024-06-19 10:23 ` [PATCH v6 0/8] dts: add testpmd params Luca Vizzarro
                     ` (4 preceding siblings ...)
  2024-06-19 10:23   ` [PATCH v6 5/8] dts: add testpmd shell params Luca Vizzarro
@ 2024-06-19 10:23   ` Luca Vizzarro
  2024-06-19 10:23   ` [PATCH v6 7/8] dts: rework interactive shells Luca Vizzarro
  2024-06-19 10:23   ` [PATCH v6 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
  7 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-19 10:23 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Update the buffer scatter test suite to use TestPmdParameters
instead of the StrParams implementation.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 dts/tests/TestSuite_pmd_buffer_scatter.py | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index 578b5a4318..6d206c1a40 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -16,14 +16,14 @@
 """
 
 import struct
+from dataclasses import asdict
 
 from scapy.layers.inet import IP  # type: ignore[import-untyped]
 from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
 from scapy.packet import Raw  # type: ignore[import-untyped]
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
-from framework.params import Params
-from framework.params.testpmd import SimpleForwardingModes
+from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
 from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.test_suite import TestSuite
 
@@ -105,16 +105,16 @@ def pmd_scatter(self, mbsize: int) -> None:
         """
         testpmd = self.sut_node.create_interactive_shell(
             TestPmdShell,
-            app_params=Params.from_str(
-                "--mbcache=200 "
-                f"--mbuf-size={mbsize} "
-                "--max-pkt-len=9000 "
-                "--port-topology=paired "
-                "--tx-offloads=0x00008000"
+            app_params=TestPmdParams(
+                forward_mode=SimpleForwardingModes.mac,
+                mbcache=200,
+                mbuf_size=[mbsize],
+                max_pkt_len=9000,
+                tx_offloads=0x00008000,
+                **asdict(self.sut_node.create_eal_parameters()),
             ),
             privileged=True,
         )
-        testpmd.set_forward_mode(SimpleForwardingModes.mac)
         testpmd.start()
 
         for offset in [-1, 0, 1, 4, 5]:
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v6 7/8] dts: rework interactive shells
  2024-06-19 10:23 ` [PATCH v6 0/8] dts: add testpmd params Luca Vizzarro
                     ` (5 preceding siblings ...)
  2024-06-19 10:23   ` [PATCH v6 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
@ 2024-06-19 10:23   ` Luca Vizzarro
  2024-06-19 12:49     ` Juraj Linkeš
  2024-06-19 10:23   ` [PATCH v6 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
  7 siblings, 1 reply; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-19 10:23 UTC (permalink / raw)
  To: dev; +Cc: Jeremy Spewock, Juraj Linkeš, Luca Vizzarro, Paul Szczepanek

The way nodes and interactive shells interact makes it difficult to
develop for static type checking and hinting. The current system relies
on a top-down approach, attempting to give a generic interface to the
test developer, hiding the interaction of concrete shell classes as much
as possible. When working with strong typing this approach is not ideal,
as Python's implementation of generics is still rudimentary.

This rework reverses the tests interaction to a bottom-up approach,
allowing the test developer to call concrete shell classes directly,
and let them ingest nodes independently. While also re-enforcing type
checking and making the code easier to read.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 dts/framework/params/eal.py                   |   6 +-
 dts/framework/remote_session/dpdk_shell.py    | 105 +++++++++++++++
 .../remote_session/interactive_shell.py       |  75 ++++++-----
 dts/framework/remote_session/python_shell.py  |   4 +-
 dts/framework/remote_session/testpmd_shell.py |  64 +++++----
 dts/framework/testbed_model/node.py           |  36 +----
 dts/framework/testbed_model/os_session.py     |  36 +----
 dts/framework/testbed_model/sut_node.py       | 124 +-----------------
 .../testbed_model/traffic_generator/scapy.py  |   4 +-
 dts/tests/TestSuite_hello_world.py            |   7 +-
 dts/tests/TestSuite_pmd_buffer_scatter.py     |  21 ++-
 dts/tests/TestSuite_smoke_tests.py            |   2 +-
 12 files changed, 205 insertions(+), 279 deletions(-)
 create mode 100644 dts/framework/remote_session/dpdk_shell.py

diff --git a/dts/framework/params/eal.py b/dts/framework/params/eal.py
index bbdbc8f334..8d7766fefc 100644
--- a/dts/framework/params/eal.py
+++ b/dts/framework/params/eal.py
@@ -35,9 +35,9 @@ class EalParams(Params):
                 ``other_eal_param='--single-file-segments'``
     """
 
-    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
-    memory_channels: int = field(metadata=Params.short("n"))
-    prefix: str = field(metadata=Params.long("file-prefix"))
+    lcore_list: LogicalCoreList | None = field(default=None, metadata=Params.short("l"))
+    memory_channels: int | None = field(default=None, metadata=Params.short("n"))
+    prefix: str = field(default="dpdk", metadata=Params.long("file-prefix"))
     no_pci: Switch = None
     vdevs: list[VirtualDevice] | None = field(
         default=None, metadata=Params.multiple() | Params.long("vdev")
diff --git a/dts/framework/remote_session/dpdk_shell.py b/dts/framework/remote_session/dpdk_shell.py
new file mode 100644
index 0000000000..296639f37d
--- /dev/null
+++ b/dts/framework/remote_session/dpdk_shell.py
@@ -0,0 +1,105 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Base interactive shell for DPDK applications.
+
+Provides a base class to create interactive shells based on DPDK.
+"""
+
+
+from abc import ABC
+from pathlib import PurePath
+
+from framework.params.eal import EalParams
+from framework.remote_session.interactive_shell import InteractiveShell
+from framework.settings import SETTINGS
+from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
+from framework.testbed_model.sut_node import SutNode
+
+
+def compute_eal_params(
+    sut_node: SutNode,
+    params: EalParams | None = None,
+    lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
+    ascending_cores: bool = True,
+    append_prefix_timestamp: bool = True,
+) -> EalParams:
+    """Compute EAL parameters based on the node's specifications.
+
+    Args:
+        sut_node: The SUT node to compute the values for.
+        params: If :data:`None`, a new object is created and returned. Otherwise `params.lcore_list`
+            is modified according to `lcore_filter_specifier`. A DPDK file prefix is also added. If
+            `params.ports` is :data:`None`, then `sut_node.ports` is assigned to it.
+        lcore_filter_specifier: A number of lcores/cores/sockets to use or a list of lcore ids to
+            use. The default will select one lcore for each of two cores on one socket, in ascending
+            order of core ids.
+        ascending_cores: Sort cores in ascending order (lowest to highest IDs). If :data:`False`,
+            sort in descending order.
+        append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.
+    """
+    if params is None:
+        params = EalParams()
+
+    if params.lcore_list is None:
+        params.lcore_list = LogicalCoreList(
+            sut_node.filter_lcores(lcore_filter_specifier, ascending_cores)
+        )
+
+    prefix = params.prefix
+    if append_prefix_timestamp:
+        prefix = f"{prefix}_{sut_node.dpdk_timestamp}"
+    prefix = sut_node.main_session.get_dpdk_file_prefix(prefix)
+    if prefix:
+        sut_node.dpdk_prefix_list.append(prefix)
+    params.prefix = prefix
+
+    if params.ports is None:
+        params.ports = sut_node.ports
+
+    return params
+
+
+class DPDKShell(InteractiveShell, ABC):
+    """The base class for managing DPDK-based interactive shells.
+
+    This class shouldn't be instantiated directly, but instead be extended.
+    It automatically injects computed EAL parameters based on the node in the
+    supplied app parameters.
+    """
+
+    _node: SutNode
+    _app_params: EalParams
+
+    def __init__(
+        self,
+        node: SutNode,
+        privileged: bool = True,
+        timeout: float = SETTINGS.timeout,
+        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
+        ascending_cores: bool = True,
+        append_prefix_timestamp: bool = True,
+        start_on_init: bool = True,
+        app_params: EalParams = EalParams(),
+    ) -> None:
+        """Extends :meth:`~.interactive_shell.InteractiveShell.__init__`.
+
+        Adds the `lcore_filter_specifier`, `ascending_cores` and `append_prefix_timestamp` arguments
+        which are then used to compute the EAL parameters based on the node's configuration.
+        """
+        app_params = compute_eal_params(
+            node,
+            app_params,
+            lcore_filter_specifier,
+            ascending_cores,
+            append_prefix_timestamp,
+        )
+
+        super().__init__(node, privileged, timeout, start_on_init, app_params)
+
+    def _update_real_path(self, path: PurePath) -> None:
+        """Extends :meth:`~.interactive_shell.InteractiveShell._update_real_path`.
+
+        Adds the remote DPDK build directory to the path.
+        """
+        super()._update_real_path(self._node.remote_dpdk_build_dir.joinpath(path))
diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
index 8191b36630..254aa29f89 100644
--- a/dts/framework/remote_session/interactive_shell.py
+++ b/dts/framework/remote_session/interactive_shell.py
@@ -17,13 +17,14 @@
 
 from abc import ABC
 from pathlib import PurePath
-from typing import Callable, ClassVar
+from typing import ClassVar
 
-from paramiko import Channel, SSHClient, channel  # type: ignore[import-untyped]
+from paramiko import Channel, channel  # type: ignore[import-untyped]
 
 from framework.logger import DTSLogger
 from framework.params import Params
 from framework.settings import SETTINGS
+from framework.testbed_model.node import Node
 
 
 class InteractiveShell(ABC):
@@ -36,13 +37,15 @@ class InteractiveShell(ABC):
     session.
     """
 
-    _interactive_session: SSHClient
+    _node: Node
     _stdin: channel.ChannelStdinFile
     _stdout: channel.ChannelFile
     _ssh_channel: Channel
     _logger: DTSLogger
     _timeout: float
     _app_params: Params
+    _privileged: bool
+    _real_path: PurePath
 
     #: Prompt to expect at the end of output when sending a command.
     #: This is often overridden by subclasses.
@@ -56,56 +59,58 @@ class InteractiveShell(ABC):
     #: Path to the executable to start the interactive application.
     path: ClassVar[PurePath]
 
-    #: Whether this application is a DPDK app. If it is, the build directory
-    #: for DPDK on the node will be prepended to the path to the executable.
-    dpdk_app: ClassVar[bool] = False
-
     def __init__(
         self,
-        interactive_session: SSHClient,
-        logger: DTSLogger,
-        get_privileged_command: Callable[[str], str] | None,
-        app_params: Params = Params(),
+        node: Node,
+        privileged: bool = False,
         timeout: float = SETTINGS.timeout,
+        start_on_init: bool = True,
+        app_params: Params = Params(),
     ) -> None:
         """Create an SSH channel during initialization.
 
         Args:
-            interactive_session: The SSH session dedicated to interactive shells.
-            logger: The logger instance this session will use.
-            get_privileged_command: A method for modifying a command to allow it to use
-                elevated privileges. If :data:`None`, the application will not be started
-                with elevated privileges.
-            app_params: The command line parameters to be passed to the application on startup.
+            node: The node on which to run start the interactive shell.
+            privileged: Enables the shell to run as superuser.
             timeout: The timeout used for the SSH channel that is dedicated to this interactive
                 shell. This timeout is for collecting output, so if reading from the buffer
                 and no output is gathered within the timeout, an exception is thrown.
+            start_on_init: Start interactive shell automatically after object initialisation.
+            app_params: The command line parameters to be passed to the application on startup.
         """
-        self._interactive_session = interactive_session
-        self._ssh_channel = self._interactive_session.invoke_shell()
+        self._node = node
+        self._logger = node._logger
+        self._app_params = app_params
+        self._privileged = privileged
+        self._timeout = timeout
+        # Ensure path is properly formatted for the host
+        self._update_real_path(self.path)
+
+        if start_on_init:
+            self.start_application()
+
+    def _setup_ssh_channel(self):
+        self._ssh_channel = self._node.main_session.interactive_session.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.settimeout(self._timeout)
         self._ssh_channel.set_combine_stderr(True)  # combines stdout and stderr streams
-        self._logger = logger
-        self._timeout = timeout
-        self._app_params = app_params
-        self._start_application(get_privileged_command)
 
-    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
+    def _make_start_command(self) -> str:
+        """Makes the command that starts the interactive shell."""
+        start_command = f"{self._real_path} {self._app_params or ''}"
+        if self._privileged:
+            start_command = self._node.main_session._get_privileged_command(start_command)
+        return start_command
+
+    def start_application(self) -> None:
         """Starts a new interactive application based on the path to the app.
 
         This method is often overridden by subclasses as their process for
         starting may look different.
-
-        Args:
-            get_privileged_command: A function (but could be any callable) that produces
-                the version of the command with elevated privileges.
         """
-        start_command = f"{self.path} {self._app_params}"
-        if get_privileged_command is not None:
-            start_command = get_privileged_command(start_command)
-        self.send_command(start_command)
+        self._setup_ssh_channel()
+        self.send_command(self._make_start_command())
 
     def send_command(
         self, command: str, prompt: str | None = None, skip_first_line: bool = False
@@ -156,3 +161,7 @@ def close(self) -> None:
     def __del__(self) -> None:
         """Make sure the session is properly closed before deleting the object."""
         self.close()
+
+    def _update_real_path(self, path: PurePath) -> None:
+        """Updates the interactive shell's real path used at command line."""
+        self._real_path = self._node.main_session.join_remote_path(path)
diff --git a/dts/framework/remote_session/python_shell.py b/dts/framework/remote_session/python_shell.py
index ccfd3783e8..953ed100df 100644
--- a/dts/framework/remote_session/python_shell.py
+++ b/dts/framework/remote_session/python_shell.py
@@ -6,9 +6,7 @@
 Typical usage example in a TestSuite::
 
     from framework.remote_session import PythonShell
-    python_shell = self.tg_node.create_interactive_shell(
-        PythonShell, timeout=5, privileged=True
-    )
+    python_shell = PythonShell(self.tg_node, timeout=5, privileged=True)
     python_shell.send_command("print('Hello World')")
     python_shell.close()
 """
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index 82701a9839..8ee6829067 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -7,9 +7,7 @@
 
 Typical usage example in a TestSuite::
 
-    testpmd_shell = self.sut_node.create_interactive_shell(
-            TestPmdShell, privileged=True
-        )
+    testpmd_shell = TestPmdShell(self.sut_node)
     devices = testpmd_shell.get_devices()
     for device in devices:
         print(device)
@@ -21,18 +19,19 @@
 from dataclasses import dataclass, field
 from enum import Flag, auto
 from pathlib import PurePath
-from typing import Callable, ClassVar
+from typing import ClassVar
 
 from typing_extensions import Self
 
 from framework.exception import InteractiveCommandExecutionError
 from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
 from framework.parser import ParserFn, TextParser
+from framework.remote_session.dpdk_shell import DPDKShell
 from framework.settings import SETTINGS
+from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
+from framework.testbed_model.sut_node import SutNode
 from framework.utils import StrEnum
 
-from .interactive_shell import InteractiveShell
-
 
 class TestPmdDevice(object):
     """The data of a device that testpmd can recognize.
@@ -577,52 +576,48 @@ class TestPmdPortStats(TextParser):
     tx_bps: int = field(metadata=TextParser.find_int(r"Tx-bps:\s+(\d+)"))
 
 
-class TestPmdShell(InteractiveShell):
+class TestPmdShell(DPDKShell):
     """Testpmd interactive shell.
 
     The testpmd shell users should never use
     the :meth:`~.interactive_shell.InteractiveShell.send_command` method directly, but rather
     call specialized methods. If there isn't one that satisfies a need, it should be added.
-
-    Attributes:
-        number_of_ports: The number of ports which were allowed on the command-line when testpmd
-            was started.
     """
 
-    number_of_ports: int
+    _app_params: TestPmdParams
 
     #: The path to the testpmd executable.
     path: ClassVar[PurePath] = PurePath("app", "dpdk-testpmd")
 
-    #: Flag this as a DPDK app so that it's clear this is not a system app and
-    #: needs to be looked in a specific path.
-    dpdk_app: ClassVar[bool] = True
-
     #: The testpmd's prompt.
     _default_prompt: ClassVar[str] = "testpmd>"
 
     #: This forces the prompt to appear after sending a command.
     _command_extra_chars: ClassVar[str] = "\n"
 
-    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
-        """Overrides :meth:`~.interactive_shell._start_application`.
-
-        Add flags for starting testpmd in interactive mode and disabling messages for link state
-        change events before starting the application. Link state is verified before starting
-        packet forwarding and the messages create unexpected newlines in the terminal which
-        complicates output collection.
-
-        Also find the number of pci addresses which were allowed on the command line when the app
-        was started.
-        """
-        assert isinstance(self._app_params, TestPmdParams)
-
-        self.number_of_ports = (
-            len(self._app_params.ports) if self._app_params.ports is not None else 0
+    def __init__(
+        self,
+        node: SutNode,
+        privileged: bool = True,
+        timeout: float = SETTINGS.timeout,
+        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
+        ascending_cores: bool = True,
+        append_prefix_timestamp: bool = True,
+        start_on_init: bool = True,
+        **app_params,
+    ) -> None:
+        """Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs."""
+        super().__init__(
+            node,
+            privileged,
+            timeout,
+            lcore_filter_specifier,
+            ascending_cores,
+            append_prefix_timestamp,
+            start_on_init,
+            TestPmdParams(**app_params),
         )
 
-        super()._start_application(get_privileged_command)
-
     def start(self, verify: bool = True) -> None:
         """Start packet forwarding with the current configuration.
 
@@ -642,7 +637,8 @@ def start(self, verify: bool = True) -> None:
                 self._logger.debug(f"Failed to start packet forwarding: \n{start_cmd_output}")
                 raise InteractiveCommandExecutionError("Testpmd failed to start packet forwarding.")
 
-            for port_id in range(self.number_of_ports):
+            number_of_ports = len(self._app_params.ports or [])
+            for port_id in range(number_of_ports):
                 if not self.wait_link_status_up(port_id):
                     raise InteractiveCommandExecutionError(
                         "Not all ports came up after starting packet forwarding in testpmd."
diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
index 6af4f25a3c..88395faabe 100644
--- a/dts/framework/testbed_model/node.py
+++ b/dts/framework/testbed_model/node.py
@@ -15,7 +15,7 @@
 
 from abc import ABC
 from ipaddress import IPv4Interface, IPv6Interface
-from typing import Any, Callable, Type, Union
+from typing import Any, Callable, Union
 
 from framework.config import (
     OS,
@@ -25,7 +25,6 @@
 )
 from framework.exception import ConfigurationError
 from framework.logger import DTSLogger, get_dts_logger
-from framework.params import Params
 from framework.settings import SETTINGS
 
 from .cpu import (
@@ -36,7 +35,7 @@
     lcore_filter,
 )
 from .linux_session import LinuxSession
-from .os_session import InteractiveShellType, OSSession
+from .os_session import OSSession
 from .port import Port
 from .virtual_device import VirtualDevice
 
@@ -196,37 +195,6 @@ def create_session(self, name: str) -> OSSession:
         self._other_sessions.append(connection)
         return connection
 
-    def create_interactive_shell(
-        self,
-        shell_cls: Type[InteractiveShellType],
-        timeout: float = SETTINGS.timeout,
-        privileged: bool = False,
-        app_params: Params = Params(),
-    ) -> InteractiveShellType:
-        """Factory for interactive session handlers.
-
-        Instantiate `shell_cls` according to the remote OS specifics.
-
-        Args:
-            shell_cls: The class of the shell.
-            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.
-            privileged: Whether to run the shell with administrative privileges.
-            app_args: The arguments to be passed to the application.
-
-        Returns:
-            An instance of the desired interactive application shell.
-        """
-        if not shell_cls.dpdk_app:
-            shell_cls.path = self.main_session.join_remote_path(shell_cls.path)
-
-        return self.main_session.create_interactive_shell(
-            shell_cls,
-            timeout,
-            privileged,
-            app_params,
-        )
-
     def filter_lcores(
         self,
         filter_specifier: LogicalCoreCount | LogicalCoreList,
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
index e5f5fcbe0e..e7e6c9d670 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -26,18 +26,16 @@
 from collections.abc import Iterable
 from ipaddress import IPv4Interface, IPv6Interface
 from pathlib import PurePath
-from typing import Type, TypeVar, Union
+from typing import Union
 
 from framework.config import Architecture, NodeConfiguration, NodeInfo
 from framework.logger import DTSLogger
-from framework.params import Params
 from framework.remote_session import (
     InteractiveRemoteSession,
     RemoteSession,
     create_interactive_session,
     create_remote_session,
 )
-from framework.remote_session.interactive_shell import InteractiveShell
 from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
@@ -45,8 +43,6 @@
 from .cpu import LogicalCore
 from .port import Port
 
-InteractiveShellType = TypeVar("InteractiveShellType", bound=InteractiveShell)
-
 
 class OSSession(ABC):
     """OS-unaware to OS-aware translation API definition.
@@ -131,36 +127,6 @@ def send_command(
 
         return self.remote_session.send_command(command, timeout, verify, env)
 
-    def create_interactive_shell(
-        self,
-        shell_cls: Type[InteractiveShellType],
-        timeout: float,
-        privileged: bool,
-        app_args: Params,
-    ) -> InteractiveShellType:
-        """Factory for interactive session handlers.
-
-        Instantiate `shell_cls` according to the remote OS specifics.
-
-        Args:
-            shell_cls: The class of the shell.
-            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.
-            privileged: Whether to run the shell with administrative privileges.
-            app_args: The arguments to be passed to the application.
-
-        Returns:
-            An instance of the desired interactive application shell.
-        """
-        return shell_cls(
-            self.interactive_session.session,
-            self._logger,
-            self._get_privileged_command if privileged else None,
-            app_args,
-            timeout,
-        )
-
     @staticmethod
     @abstractmethod
     def _get_privileged_command(command: str) -> str:
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index 83ad06ae2d..d231a01425 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -16,7 +16,6 @@
 import tarfile
 import time
 from pathlib import PurePath
-from typing import Type
 
 from framework.config import (
     BuildTargetConfiguration,
@@ -24,17 +23,13 @@
     NodeInfo,
     SutNodeConfiguration,
 )
-from framework.params import Params, Switch
 from framework.params.eal import EalParams
 from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
 
-from .cpu import LogicalCoreCount, LogicalCoreList
 from .node import Node
-from .os_session import InteractiveShellType, OSSession
-from .port import Port
-from .virtual_device import VirtualDevice
+from .os_session import OSSession
 
 
 class SutNode(Node):
@@ -56,8 +51,8 @@ class SutNode(Node):
     """
 
     config: SutNodeConfiguration
-    _dpdk_prefix_list: list[str]
-    _dpdk_timestamp: str
+    dpdk_prefix_list: list[str]
+    dpdk_timestamp: str
     _build_target_config: BuildTargetConfiguration | None
     _env_vars: dict
     _remote_tmp_dir: PurePath
@@ -76,14 +71,14 @@ def __init__(self, node_config: SutNodeConfiguration):
             node_config: The SUT node's test run configuration.
         """
         super(SutNode, self).__init__(node_config)
-        self._dpdk_prefix_list = []
+        self.dpdk_prefix_list = []
         self._build_target_config = None
         self._env_vars = {}
         self._remote_tmp_dir = self.main_session.get_remote_tmp_dir()
         self.__remote_dpdk_dir = None
         self._app_compile_timeout = 90
         self._dpdk_kill_session = None
-        self._dpdk_timestamp = (
+        self.dpdk_timestamp = (
             f"{str(os.getpid())}_{time.strftime('%Y%m%d%H%M%S', time.localtime())}"
         )
         self._dpdk_version = None
@@ -283,73 +278,11 @@ def kill_cleanup_dpdk_apps(self) -> None:
         """Kill all dpdk applications on the SUT, then clean up hugepages."""
         if self._dpdk_kill_session and self._dpdk_kill_session.is_alive():
             # we can use the session if it exists and responds
-            self._dpdk_kill_session.kill_cleanup_dpdk_apps(self._dpdk_prefix_list)
+            self._dpdk_kill_session.kill_cleanup_dpdk_apps(self.dpdk_prefix_list)
         else:
             # otherwise, we need to (re)create it
             self._dpdk_kill_session = self.create_session("dpdk_kill")
-        self._dpdk_prefix_list = []
-
-    def create_eal_parameters(
-        self,
-        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
-        ascending_cores: bool = True,
-        prefix: str = "dpdk",
-        append_prefix_timestamp: bool = True,
-        no_pci: Switch = None,
-        vdevs: list[VirtualDevice] | None = None,
-        ports: list[Port] | None = None,
-        other_eal_param: str = "",
-    ) -> EalParams:
-        """Compose the EAL parameters.
-
-        Process the list of cores and the DPDK prefix and pass that along with
-        the rest of the arguments.
-
-        Args:
-            lcore_filter_specifier: A number of lcores/cores/sockets to use
-                or a list of lcore ids to use.
-                The default will select one lcore for each of two cores
-                on one socket, in ascending order of core ids.
-            ascending_cores: Sort cores in ascending order (lowest to highest IDs).
-                If :data:`False`, sort in descending order.
-            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
-            append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.
-            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
-            vdevs: Virtual devices, e.g.::
-
-                vdevs=[
-                    VirtualDevice('net_ring0'),
-                    VirtualDevice('net_ring1')
-                ]
-            ports: The list of ports to allow. If :data:`None`, all ports listed in `self.ports`
-                will be allowed.
-            other_eal_param: user defined DPDK EAL parameters, e.g.:
-                ``other_eal_param='--single-file-segments'``.
-
-        Returns:
-            An EAL param string, such as
-            ``-c 0xf -a 0000:88:00.0 --file-prefix=dpdk_1112_20190809143420``.
-        """
-        lcore_list = LogicalCoreList(self.filter_lcores(lcore_filter_specifier, ascending_cores))
-
-        if append_prefix_timestamp:
-            prefix = f"{prefix}_{self._dpdk_timestamp}"
-        prefix = self.main_session.get_dpdk_file_prefix(prefix)
-        if prefix:
-            self._dpdk_prefix_list.append(prefix)
-
-        if ports is None:
-            ports = self.ports
-
-        return EalParams(
-            lcore_list=lcore_list,
-            memory_channels=self.config.memory_channels,
-            prefix=prefix,
-            no_pci=no_pci,
-            vdevs=vdevs,
-            ports=ports,
-            other_eal_param=Params.from_str(other_eal_param),
-        )
+        self.dpdk_prefix_list = []
 
     def run_dpdk_app(
         self, app_path: PurePath, eal_params: EalParams, timeout: float = 30
@@ -379,49 +312,6 @@ def configure_ipv4_forwarding(self, enable: bool) -> None:
         """
         self.main_session.configure_ipv4_forwarding(enable)
 
-    def create_interactive_shell(
-        self,
-        shell_cls: Type[InteractiveShellType],
-        timeout: float = SETTINGS.timeout,
-        privileged: bool = False,
-        app_params: Params = Params(),
-        eal_params: EalParams | None = None,
-    ) -> InteractiveShellType:
-        """Extend the factory for interactive session handlers.
-
-        The extensions are SUT node specific:
-
-            * The default for `eal_parameters`,
-            * The interactive shell path `shell_cls.path` is prepended with path to the remote
-              DPDK build directory for DPDK apps.
-
-        Args:
-            shell_cls: The class of the shell.
-            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.
-            privileged: Whether to run the shell with administrative privileges.
-            app_params: The parameters to be passed to the application.
-            eal_params: List of EAL parameters to use to launch the app. If this
-                isn't provided or an empty string is passed, it will default to calling
-                :meth:`create_eal_parameters`.
-
-        Returns:
-            An instance of the desired interactive application shell.
-        """
-        # We need to append the build directory and add EAL parameters for DPDK apps
-        if shell_cls.dpdk_app:
-            if eal_params is None:
-                eal_params = self.create_eal_parameters()
-            eal_params.append_str(str(app_params))
-            app_params = eal_params
-
-            shell_cls.path = self.main_session.join_remote_path(
-                self.remote_dpdk_build_dir, shell_cls.path
-            )
-
-        return super().create_interactive_shell(shell_cls, timeout, privileged, app_params)
-
     def bind_ports_to_driver(self, for_dpdk: bool = True) -> None:
         """Bind all ports on the SUT to a driver.
 
diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py
index 7bc1c2cc08..bf58ad1c5e 100644
--- a/dts/framework/testbed_model/traffic_generator/scapy.py
+++ b/dts/framework/testbed_model/traffic_generator/scapy.py
@@ -217,9 +217,7 @@ def __init__(self, tg_node: Node, config: ScapyTrafficGeneratorConfig):
             self._tg_node.config.os == OS.linux
         ), "Linux is the only supported OS for scapy traffic generation"
 
-        self.session = self._tg_node.create_interactive_shell(
-            PythonShell, timeout=5, privileged=True
-        )
+        self.session = PythonShell(self._tg_node, timeout=5, privileged=True)
 
         # import libs in remote python console
         for import_statement in SCAPY_RPC_SERVER_IMPORTS:
diff --git a/dts/tests/TestSuite_hello_world.py b/dts/tests/TestSuite_hello_world.py
index 0d6995f260..d958f99030 100644
--- a/dts/tests/TestSuite_hello_world.py
+++ b/dts/tests/TestSuite_hello_world.py
@@ -7,6 +7,7 @@
 No other EAL parameters apart from cores are used.
 """
 
+from framework.remote_session.dpdk_shell import compute_eal_params
 from framework.test_suite import TestSuite
 from framework.testbed_model.cpu import (
     LogicalCoreCount,
@@ -38,7 +39,7 @@ def test_hello_world_single_core(self) -> None:
         # get the first usable core
         lcore_amount = LogicalCoreCount(1, 1, 1)
         lcores = LogicalCoreCountFilter(self.sut_node.lcores, lcore_amount).filter()
-        eal_para = self.sut_node.create_eal_parameters(lcore_filter_specifier=lcore_amount)
+        eal_para = compute_eal_params(self.sut_node, lcore_filter_specifier=lcore_amount)
         result = self.sut_node.run_dpdk_app(self.app_helloworld_path, eal_para)
         self.verify(
             f"hello from core {int(lcores[0])}" in result.stdout,
@@ -55,8 +56,8 @@ def test_hello_world_all_cores(self) -> None:
             "hello from core <core_id>"
         """
         # get the maximum logical core number
-        eal_para = self.sut_node.create_eal_parameters(
-            lcore_filter_specifier=LogicalCoreList(self.sut_node.lcores)
+        eal_para = compute_eal_params(
+            self.sut_node, lcore_filter_specifier=LogicalCoreList(self.sut_node.lcores)
         )
         result = self.sut_node.run_dpdk_app(self.app_helloworld_path, eal_para, 50)
         for lcore in self.sut_node.lcores:
diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index 6d206c1a40..43cf5c61eb 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -16,14 +16,13 @@
 """
 
 import struct
-from dataclasses import asdict
 
 from scapy.layers.inet import IP  # type: ignore[import-untyped]
 from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
 from scapy.packet import Raw  # type: ignore[import-untyped]
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
-from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
+from framework.params.testpmd import SimpleForwardingModes
 from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.test_suite import TestSuite
 
@@ -103,17 +102,13 @@ def pmd_scatter(self, mbsize: int) -> None:
         Test:
             Start testpmd and run functional test with preset mbsize.
         """
-        testpmd = self.sut_node.create_interactive_shell(
-            TestPmdShell,
-            app_params=TestPmdParams(
-                forward_mode=SimpleForwardingModes.mac,
-                mbcache=200,
-                mbuf_size=[mbsize],
-                max_pkt_len=9000,
-                tx_offloads=0x00008000,
-                **asdict(self.sut_node.create_eal_parameters()),
-            ),
-            privileged=True,
+        testpmd = TestPmdShell(
+            self.sut_node,
+            forward_mode=SimpleForwardingModes.mac,
+            mbcache=200,
+            mbuf_size=[mbsize],
+            max_pkt_len=9000,
+            tx_offloads=0x00008000,
         )
         testpmd.start()
 
diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py
index ca678f662d..eca27acfd8 100644
--- a/dts/tests/TestSuite_smoke_tests.py
+++ b/dts/tests/TestSuite_smoke_tests.py
@@ -99,7 +99,7 @@ def test_devices_listed_in_testpmd(self) -> None:
         Test:
             List all devices found in testpmd and verify the configured devices are among them.
         """
-        testpmd_driver = self.sut_node.create_interactive_shell(TestPmdShell, privileged=True)
+        testpmd_driver = TestPmdShell(self.sut_node)
         dev_list = [str(x) for x in testpmd_driver.get_devices()]
         for nic in self.nics_in_node:
             self.verify(
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v6 8/8] dts: use Unpack for type checking and hinting
  2024-06-19 10:23 ` [PATCH v6 0/8] dts: add testpmd params Luca Vizzarro
                     ` (6 preceding siblings ...)
  2024-06-19 10:23   ` [PATCH v6 7/8] dts: rework interactive shells Luca Vizzarro
@ 2024-06-19 10:23   ` Luca Vizzarro
  7 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-19 10:23 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Interactive shells that inherit DPDKShell initialise their params
classes from a kwargs dict. Therefore, static type checking is
disabled. This change uses the functionality of Unpack added in
PEP 692 to re-enable it. The disadvantage is that this functionality has
been implemented only with TypedDict, forcing the creation of TypedDict
mirrors of the Params classes.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 dts/framework/params/types.py                 | 133 ++++++++++++++++++
 dts/framework/remote_session/testpmd_shell.py |   5 +-
 2 files changed, 136 insertions(+), 2 deletions(-)
 create mode 100644 dts/framework/params/types.py

diff --git a/dts/framework/params/types.py b/dts/framework/params/types.py
new file mode 100644
index 0000000000..e668f658d8
--- /dev/null
+++ b/dts/framework/params/types.py
@@ -0,0 +1,133 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Module containing TypeDict-equivalents of Params classes for static typing and hinting.
+
+TypedDicts can be used in conjunction with Unpack and kwargs for type hinting on function calls.
+
+Example:
+    ..code:: python
+        def create_testpmd(**kwargs: Unpack[TestPmdParamsDict]):
+            params = TestPmdParams(**kwargs)
+"""
+
+from pathlib import PurePath
+from typing import TypedDict
+
+from framework.params import Switch, YesNoSwitch
+from framework.params.testpmd import (
+    AnonMempoolAllocationMode,
+    EthPeer,
+    Event,
+    FlowGenForwardingMode,
+    HairpinMode,
+    NoisyForwardingMode,
+    Params,
+    PortNUMAConfig,
+    PortTopology,
+    RingNUMAConfig,
+    RSSSetting,
+    RXMultiQueueMode,
+    RXRingParams,
+    SimpleForwardingModes,
+    SimpleMempoolAllocationMode,
+    TxIPAddrPair,
+    TXOnlyForwardingMode,
+    TXRingParams,
+    TxUDPPortPair,
+)
+from framework.testbed_model.cpu import LogicalCoreList
+from framework.testbed_model.port import Port
+from framework.testbed_model.virtual_device import VirtualDevice
+
+
+class EalParamsDict(TypedDict, total=False):
+    """:class:`TypedDict` equivalent of :class:`~.eal.EalParams`."""
+
+    lcore_list: LogicalCoreList | None
+    memory_channels: int | None
+    prefix: str
+    no_pci: Switch
+    vdevs: list[VirtualDevice] | None
+    ports: list[Port] | None
+    other_eal_param: Params | None
+
+
+class TestPmdParamsDict(EalParamsDict, total=False):
+    """:class:`TypedDict` equivalent of :class:`~.testpmd.TestPmdParams`."""
+
+    interactive_mode: Switch
+    auto_start: Switch
+    tx_first: Switch
+    stats_period: int | None
+    display_xstats: list[str] | None
+    nb_cores: int | None
+    coremask: int | None
+    nb_ports: int | None
+    port_topology: PortTopology | None
+    portmask: int | None
+    portlist: str | None
+    numa: YesNoSwitch
+    socket_num: int | None
+    port_numa_config: list[PortNUMAConfig] | None
+    ring_numa_config: list[RingNUMAConfig] | None
+    total_num_mbufs: int | None
+    mbuf_size: list[int] | None
+    mbcache: int | None
+    max_pkt_len: int | None
+    eth_peers_configfile: PurePath | None
+    eth_peer: list[EthPeer] | None
+    tx_ip: TxIPAddrPair | None
+    tx_udp: TxUDPPortPair | None
+    enable_lro: Switch
+    max_lro_pkt_size: int | None
+    disable_crc_strip: Switch
+    enable_scatter: Switch
+    enable_hw_vlan: Switch
+    enable_hw_vlan_filter: Switch
+    enable_hw_vlan_strip: Switch
+    enable_hw_vlan_extend: Switch
+    enable_hw_qinq_strip: Switch
+    pkt_drop_enabled: Switch
+    rss: RSSSetting | None
+    forward_mode: (
+        SimpleForwardingModes
+        | FlowGenForwardingMode
+        | TXOnlyForwardingMode
+        | NoisyForwardingMode
+        | None
+    )
+    hairpin_mode: HairpinMode | None
+    hairpin_queues: int | None
+    burst: int | None
+    enable_rx_cksum: Switch
+    rx_queues: int | None
+    rx_ring: RXRingParams | None
+    no_flush_rx: Switch
+    rx_segments_offsets: list[int] | None
+    rx_segments_length: list[int] | None
+    multi_rx_mempool: Switch
+    rx_shared_queue: Switch | int
+    rx_offloads: int | None
+    rx_mq_mode: RXMultiQueueMode | None
+    tx_queues: int | None
+    tx_ring: TXRingParams | None
+    tx_offloads: int | None
+    eth_link_speed: int | None
+    disable_link_check: Switch
+    disable_device_start: Switch
+    no_lsc_interrupt: Switch
+    no_rmv_interrupt: Switch
+    bitrate_stats: int | None
+    latencystats: int | None
+    print_events: list[Event] | None
+    mask_events: list[Event] | None
+    flow_isolate_all: Switch
+    disable_flow_flush: Switch
+    hot_plug: Switch
+    vxlan_gpe_port: int | None
+    geneve_parsed_port: int | None
+    lock_all_memory: YesNoSwitch
+    mempool_allocation_mode: SimpleMempoolAllocationMode | AnonMempoolAllocationMode | None
+    record_core_cycles: Switch
+    record_burst_status: Switch
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index 8ee6829067..96a690b6de 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -21,10 +21,11 @@
 from pathlib import PurePath
 from typing import ClassVar
 
-from typing_extensions import Self
+from typing_extensions import Self, Unpack
 
 from framework.exception import InteractiveCommandExecutionError
 from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
+from framework.params.types import TestPmdParamsDict
 from framework.parser import ParserFn, TextParser
 from framework.remote_session.dpdk_shell import DPDKShell
 from framework.settings import SETTINGS
@@ -604,7 +605,7 @@ def __init__(
         ascending_cores: bool = True,
         append_prefix_timestamp: bool = True,
         start_on_init: bool = True,
-        **app_params,
+        **app_params: Unpack[TestPmdParamsDict],
     ) -> None:
         """Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs."""
         super().__init__(
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v6 1/8] dts: add params manipulation module
  2024-06-19 10:23   ` [PATCH v6 1/8] dts: add params manipulation module Luca Vizzarro
@ 2024-06-19 12:45     ` Juraj Linkeš
  0 siblings, 0 replies; 159+ messages in thread
From: Juraj Linkeš @ 2024-06-19 12:45 UTC (permalink / raw)
  To: Luca Vizzarro, dev; +Cc: Jeremy Spewock, Paul Szczepanek



On 19. 6. 2024 12:23, Luca Vizzarro wrote:
> This commit introduces a new "params" module, which adds a new way
> to manage command line parameters. The provided Params dataclass
> is able to read the fields of its child class and produce a string
> representation to supply to the command line. Any data structure
> that is intended to represent command line parameters can inherit it.
> 
> The main purpose is to make it easier to represent data structures that
> map to parameters. Aiding quicker development, while minimising code
> bloat.
> 
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>

Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v6 7/8] dts: rework interactive shells
  2024-06-19 10:23   ` [PATCH v6 7/8] dts: rework interactive shells Luca Vizzarro
@ 2024-06-19 12:49     ` Juraj Linkeš
  0 siblings, 0 replies; 159+ messages in thread
From: Juraj Linkeš @ 2024-06-19 12:49 UTC (permalink / raw)
  To: Luca Vizzarro, dev; +Cc: Jeremy Spewock, Paul Szczepanek



On 19. 6. 2024 12:23, Luca Vizzarro wrote:
> The way nodes and interactive shells interact makes it difficult to
> develop for static type checking and hinting. The current system relies
> on a top-down approach, attempting to give a generic interface to the
> test developer, hiding the interaction of concrete shell classes as much
> as possible. When working with strong typing this approach is not ideal,
> as Python's implementation of generics is still rudimentary.
> 
> This rework reverses the tests interaction to a bottom-up approach,
> allowing the test developer to call concrete shell classes directly,
> and let them ingest nodes independently. While also re-enforcing type
> checking and making the code easier to read.
> 
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>

Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>

^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v7 0/8] dts: add testpmd params
  2024-03-26 19:04 [PATCH 0/6] dts: add testpmd params and statefulness Luca Vizzarro
                   ` (10 preceding siblings ...)
  2024-06-19 10:23 ` [PATCH v6 0/8] dts: add testpmd params Luca Vizzarro
@ 2024-06-19 14:02 ` Luca Vizzarro
  2024-06-19 14:02   ` [PATCH v7 1/8] dts: add params manipulation module Luca Vizzarro
                     ` (8 more replies)
  11 siblings, 9 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-19 14:02 UTC (permalink / raw)
  To: dev; +Cc: Jeremy Spewock, Juraj Linkeš, Luca Vizzarro

v7:
- rebased
v6:
- refactored InteractiveShell and DPDKShell constructors
- fixed docstrings
- removed some more module-wide imports
v5:
- fixed typo
v4:
- fixed up docstrings
- made refactoring changes
- removed params value only
- rebased on top of show port info/stats
v3:
- refactored InteractiveShell methods
- fixed docstrings
v2:
- refactored the params module
- strengthened typing of the params module
- moved the params module into its own package
- refactored EalParams and TestPmdParams and
  moved under the params package
- reworked interactions between nodes and shells
- refactored imports leading to circular dependencies

---
Depends-on: series-32112 ("dts: testpmd show port info/stats")
---

Luca Vizzarro (8):
  dts: add params manipulation module
  dts: use Params for interactive shells
  dts: refactor EalParams
  dts: remove module-wide imports
  dts: add testpmd shell params
  dts: use testpmd params for scatter test suite
  dts: rework interactive shells
  dts: use Unpack for type checking and hinting

 dts/framework/params/__init__.py              | 359 +++++++++++
 dts/framework/params/eal.py                   |  50 ++
 dts/framework/params/testpmd.py               | 607 ++++++++++++++++++
 dts/framework/params/types.py                 | 133 ++++
 dts/framework/remote_session/__init__.py      |   7 +-
 dts/framework/remote_session/dpdk_shell.py    | 105 +++
 .../remote_session/interactive_shell.py       |  79 ++-
 dts/framework/remote_session/python_shell.py  |   4 +-
 dts/framework/remote_session/testpmd_shell.py |  99 +--
 dts/framework/runner.py                       |   4 +-
 dts/framework/test_suite.py                   |   9 +-
 dts/framework/testbed_model/__init__.py       |   9 -
 dts/framework/testbed_model/node.py           |  36 +-
 dts/framework/testbed_model/os_session.py     |  38 +-
 dts/framework/testbed_model/sut_node.py       | 193 +-----
 dts/framework/testbed_model/tg_node.py        |   9 +-
 .../traffic_generator/__init__.py             |   7 +-
 .../testbed_model/traffic_generator/scapy.py  |   6 +-
 dts/tests/TestSuite_hello_world.py            |   9 +-
 dts/tests/TestSuite_pmd_buffer_scatter.py     |  21 +-
 dts/tests/TestSuite_smoke_tests.py            |   4 +-
 21 files changed, 1388 insertions(+), 400 deletions(-)
 create mode 100644 dts/framework/params/__init__.py
 create mode 100644 dts/framework/params/eal.py
 create mode 100644 dts/framework/params/testpmd.py
 create mode 100644 dts/framework/params/types.py
 create mode 100644 dts/framework/remote_session/dpdk_shell.py

-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v7 1/8] dts: add params manipulation module
  2024-06-19 14:02 ` [PATCH v7 0/8] dts: add testpmd params Luca Vizzarro
@ 2024-06-19 14:02   ` Luca Vizzarro
  2024-06-19 14:02   ` [PATCH v7 2/8] dts: use Params for interactive shells Luca Vizzarro
                     ` (7 subsequent siblings)
  8 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-19 14:02 UTC (permalink / raw)
  To: dev; +Cc: Jeremy Spewock, Juraj Linkeš, Luca Vizzarro, Paul Szczepanek

This commit introduces a new "params" module, which adds a new way
to manage command line parameters. The provided Params dataclass
is able to read the fields of its child class and produce a string
representation to supply to the command line. Any data structure
that is intended to represent command line parameters can inherit it.

The main purpose is to make it easier to represent data structures that
map to parameters. Aiding quicker development, while minimising code
bloat.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
---
 dts/framework/params/__init__.py | 359 +++++++++++++++++++++++++++++++
 1 file changed, 359 insertions(+)
 create mode 100644 dts/framework/params/__init__.py

diff --git a/dts/framework/params/__init__.py b/dts/framework/params/__init__.py
new file mode 100644
index 0000000000..5a6fd93053
--- /dev/null
+++ b/dts/framework/params/__init__.py
@@ -0,0 +1,359 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Parameter manipulation module.
+
+This module provides :class:`Params` which can be used to model any data structure
+that is meant to represent any command line parameters.
+"""
+
+from dataclasses import dataclass, fields
+from enum import Flag
+from typing import (
+    Any,
+    Callable,
+    Iterable,
+    Literal,
+    Reversible,
+    TypedDict,
+    TypeVar,
+    cast,
+)
+
+from typing_extensions import Self
+
+T = TypeVar("T")
+
+#: Type for a function taking one argument.
+FnPtr = Callable[[Any], Any]
+#: Type for a switch parameter.
+Switch = Literal[True, None]
+#: Type for a yes/no switch parameter.
+YesNoSwitch = Literal[True, False, None]
+
+
+def _reduce_functions(funcs: Iterable[FnPtr]) -> FnPtr:
+    """Reduces an iterable of :attr:`FnPtr` from left to right to a single function.
+
+    If the iterable is empty, the created function just returns its fed value back.
+
+    Args:
+        funcs: An iterable containing the functions to be chained from left to right.
+
+    Returns:
+        FnPtr: A function that calls the given functions from left to right.
+    """
+
+    def reduced_fn(value):
+        for fn in funcs:
+            value = fn(value)
+        return value
+
+    return reduced_fn
+
+
+def modify_str(*funcs: FnPtr) -> Callable[[T], T]:
+    """Class decorator modifying the ``__str__`` method with a function created from its arguments.
+
+    The :attr:`FnPtr`s fed to the decorator are executed from left to right in the arguments list
+    order.
+
+    Args:
+        *funcs: The functions to chain from left to right.
+
+    Returns:
+        The decorator.
+
+    Example:
+        .. code:: python
+
+            @convert_str(hex_from_flag_value)
+            class BitMask(enum.Flag):
+                A = auto()
+                B = auto()
+
+        will allow ``BitMask`` to render as a hexadecimal value.
+    """
+
+    def _class_decorator(original_class):
+        original_class.__str__ = _reduce_functions(funcs)
+        return original_class
+
+    return _class_decorator
+
+
+def comma_separated(values: Iterable[Any]) -> str:
+    """Converts an iterable into a comma-separated string.
+
+    Args:
+        values: An iterable of objects.
+
+    Returns:
+        A comma-separated list of stringified values.
+    """
+    return ",".join([str(value).strip() for value in values if value is not None])
+
+
+def bracketed(value: str) -> str:
+    """Adds round brackets to the input.
+
+    Args:
+        value: Any string.
+
+    Returns:
+        A string surrounded by round brackets.
+    """
+    return f"({value})"
+
+
+def str_from_flag_value(flag: Flag) -> str:
+    """Returns the value from a :class:`enum.Flag` as a string.
+
+    Args:
+        flag: An instance of :class:`Flag`.
+
+    Returns:
+        The stringified value of the given flag.
+    """
+    return str(flag.value)
+
+
+def hex_from_flag_value(flag: Flag) -> str:
+    """Returns the value from a :class:`enum.Flag` converted to hexadecimal.
+
+    Args:
+        flag: An instance of :class:`Flag`.
+
+    Returns:
+        The value of the given flag in hexadecimal representation.
+    """
+    return hex(flag.value)
+
+
+class ParamsModifier(TypedDict, total=False):
+    """Params modifiers dict compatible with the :func:`dataclasses.field` metadata parameter."""
+
+    #:
+    Params_short: str
+    #:
+    Params_long: str
+    #:
+    Params_multiple: bool
+    #:
+    Params_convert_value: Reversible[FnPtr]
+
+
+@dataclass
+class Params:
+    """Dataclass that renders its fields into command line arguments.
+
+    The parameter name is taken from the field name by default. The following:
+
+    .. code:: python
+
+        name: str | None = "value"
+
+    is rendered as ``--name=value``.
+
+    Through :func:`dataclasses.field` the resulting parameter can be manipulated by applying
+    this class' metadata modifier functions. These return regular dictionaries which can be combined
+    together using the pipe (OR) operator, as used in the example for :meth:`~Params.multiple`.
+
+    To use fields as switches, set the value to ``True`` to render them. If you
+    use a yes/no switch you can also set ``False`` which would render a switch
+    prefixed with ``--no-``. Examples:
+
+    .. code:: python
+
+        interactive: Switch = True  # renders --interactive
+        numa: YesNoSwitch   = False # renders --no-numa
+
+    Setting ``None`` will prevent it from being rendered. The :attr:`~Switch` type alias is provided
+    for regular switches, whereas :attr:`~YesNoSwitch` is offered for yes/no ones.
+
+    An instance of a dataclass inheriting ``Params`` can also be assigned to an attribute,
+    this helps with grouping parameters together.
+    The attribute holding the dataclass will be ignored and the latter will just be rendered as
+    expected.
+    """
+
+    _suffix = ""
+    """Holder of the plain text value of Params when called directly. A suffix for child classes."""
+
+    """========= BEGIN FIELD METADATA MODIFIER FUNCTIONS ========"""
+
+    @staticmethod
+    def short(name: str) -> ParamsModifier:
+        """Overrides any parameter name with the given short option.
+
+        Args:
+            name: The short parameter name.
+
+        Returns:
+            ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
+                the parameter short name modifier.
+
+        Example:
+            .. code:: python
+
+                logical_cores: str | None = field(default="1-4", metadata=Params.short("l"))
+
+            will render as ``-l=1-4`` instead of ``--logical-cores=1-4``.
+        """
+        return ParamsModifier(Params_short=name)
+
+    @staticmethod
+    def long(name: str) -> ParamsModifier:
+        """Overrides the inferred parameter name to the specified one.
+
+        Args:
+            name: The long parameter name.
+
+        Returns:
+            ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
+                the parameter long name modifier.
+
+        Example:
+            .. code:: python
+
+                x_name: str | None = field(default="y", metadata=Params.long("x"))
+
+            will render as ``--x=y``, but the field is accessed and modified through ``x_name``.
+        """
+        return ParamsModifier(Params_long=name)
+
+    @staticmethod
+    def multiple() -> ParamsModifier:
+        """Specifies that this parameter is set multiple times. The parameter type must be a list.
+
+        Returns:
+            ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
+                the multiple parameters modifier.
+
+        Example:
+            .. code:: python
+
+                ports: list[int] | None = field(
+                    default_factory=lambda: [0, 1, 2],
+                    metadata=Params.multiple() | Params.long("port")
+                )
+
+            will render as ``--port=0 --port=1 --port=2``.
+        """
+        return ParamsModifier(Params_multiple=True)
+
+    @staticmethod
+    def convert_value(*funcs: FnPtr) -> ParamsModifier:
+        """Takes in a variable number of functions to convert the value text representation.
+
+        Functions can be chained together, executed from left to right in the arguments list order.
+
+        Args:
+            *funcs: The functions to chain from left to right.
+
+        Returns:
+            ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
+                the convert value modifier.
+
+        Example:
+            .. code:: python
+
+                hex_bitmask: int | None = field(
+                    default=0b1101,
+                    metadata=Params.convert_value(hex) | Params.long("mask")
+                )
+
+            will render as ``--mask=0xd``.
+        """
+        return ParamsModifier(Params_convert_value=funcs)
+
+    """========= END FIELD METADATA MODIFIER FUNCTIONS ========"""
+
+    def append_str(self, text: str) -> None:
+        """Appends a string at the end of the string representation.
+
+        Args:
+            text: Any text to append at the end of the parameters string representation.
+        """
+        self._suffix += text
+
+    def __iadd__(self, text: str) -> Self:
+        """Appends a string at the end of the string representation.
+
+        Args:
+            text: Any text to append at the end of the parameters string representation.
+
+        Returns:
+            The given instance back.
+        """
+        self.append_str(text)
+        return self
+
+    @classmethod
+    def from_str(cls, text: str) -> Self:
+        """Creates a plain Params object from a string.
+
+        Args:
+            text: The string parameters.
+
+        Returns:
+            A new plain instance of :class:`Params`.
+        """
+        obj = cls()
+        obj.append_str(text)
+        return obj
+
+    @staticmethod
+    def _make_switch(
+        name: str, is_short: bool = False, is_no: bool = False, value: str | None = None
+    ) -> str:
+        """Make the string representation of the parameter.
+
+        Args:
+            name: The name of the parameters.
+            is_short: If the parameters is short or not.
+            is_no: If the parameter is negated or not.
+            value: The value of the parameter.
+
+        Returns:
+            The complete command line parameter.
+        """
+        prefix = f"{'-' if is_short else '--'}{'no-' if is_no else ''}"
+        name = name.replace("_", "-")
+        value = f"{' ' if is_short else '='}{value}" if value else ""
+        return f"{prefix}{name}{value}"
+
+    def __str__(self) -> str:
+        """Returns a string of command-line-ready arguments from the class fields."""
+        arguments: list[str] = []
+
+        for field in fields(self):
+            value = getattr(self, field.name)
+            modifiers = cast(ParamsModifier, field.metadata)
+
+            if value is None:
+                continue
+
+            if isinstance(value, Params):
+                arguments.append(str(value))
+                continue
+
+            # take the short modifier, or the long modifier, or infer from field name
+            switch_name = modifiers.get("Params_short", modifiers.get("Params_long", field.name))
+            is_short = "Params_short" in modifiers
+
+            if isinstance(value, bool):
+                arguments.append(self._make_switch(switch_name, is_short, is_no=(not value)))
+                continue
+
+            convert = _reduce_functions(modifiers.get("Params_convert_value", []))
+            multiple = modifiers.get("Params_multiple", False)
+
+            values = value if multiple else [value]
+            for value in values:
+                arguments.append(self._make_switch(switch_name, is_short, value=convert(value)))
+
+        if self._suffix:
+            arguments.append(self._suffix)
+
+        return " ".join(arguments)
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v7 2/8] dts: use Params for interactive shells
  2024-06-19 14:02 ` [PATCH v7 0/8] dts: add testpmd params Luca Vizzarro
  2024-06-19 14:02   ` [PATCH v7 1/8] dts: add params manipulation module Luca Vizzarro
@ 2024-06-19 14:02   ` Luca Vizzarro
  2024-06-19 14:03   ` [PATCH v7 3/8] dts: refactor EalParams Luca Vizzarro
                     ` (6 subsequent siblings)
  8 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-19 14:02 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Make it so that interactive shells accept an implementation of `Params`
for app arguments. Convert EalParameters to use `Params` instead.

String command line parameters can still be supplied by using the
`Params.from_str()` method.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 .../remote_session/interactive_shell.py       |  12 +-
 dts/framework/remote_session/testpmd_shell.py |  11 +-
 dts/framework/testbed_model/node.py           |   6 +-
 dts/framework/testbed_model/os_session.py     |   4 +-
 dts/framework/testbed_model/sut_node.py       | 124 ++++++++----------
 dts/tests/TestSuite_pmd_buffer_scatter.py     |   3 +-
 6 files changed, 77 insertions(+), 83 deletions(-)

diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
index c025c52ba3..8191b36630 100644
--- a/dts/framework/remote_session/interactive_shell.py
+++ b/dts/framework/remote_session/interactive_shell.py
@@ -1,5 +1,6 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright(c) 2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """Common functionality for interactive shell handling.
 
@@ -21,6 +22,7 @@
 from paramiko import Channel, SSHClient, channel  # type: ignore[import-untyped]
 
 from framework.logger import DTSLogger
+from framework.params import Params
 from framework.settings import SETTINGS
 
 
@@ -40,7 +42,7 @@ class InteractiveShell(ABC):
     _ssh_channel: Channel
     _logger: DTSLogger
     _timeout: float
-    _app_args: str
+    _app_params: Params
 
     #: Prompt to expect at the end of output when sending a command.
     #: This is often overridden by subclasses.
@@ -63,7 +65,7 @@ def __init__(
         interactive_session: SSHClient,
         logger: DTSLogger,
         get_privileged_command: Callable[[str], str] | None,
-        app_args: str = "",
+        app_params: Params = Params(),
         timeout: float = SETTINGS.timeout,
     ) -> None:
         """Create an SSH channel during initialization.
@@ -74,7 +76,7 @@ def __init__(
             get_privileged_command: A method for modifying a command to allow it to use
                 elevated privileges. If :data:`None`, the application will not be started
                 with elevated privileges.
-            app_args: The command line arguments to be passed to the application on startup.
+            app_params: The command line parameters to be passed to the application on startup.
             timeout: The timeout used for the SSH channel that is dedicated to this interactive
                 shell. This timeout is for collecting output, so if reading from the buffer
                 and no output is gathered within the timeout, an exception is thrown.
@@ -87,7 +89,7 @@ def __init__(
         self._ssh_channel.set_combine_stderr(True)  # combines stdout and stderr streams
         self._logger = logger
         self._timeout = timeout
-        self._app_args = app_args
+        self._app_params = app_params
         self._start_application(get_privileged_command)
 
     def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
@@ -100,7 +102,7 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
             get_privileged_command: A function (but could be any callable) that produces
                 the version of the command with elevated privileges.
         """
-        start_command = f"{self.path} {self._app_args}"
+        start_command = f"{self.path} {self._app_params}"
         if get_privileged_command is not None:
             start_command = get_privileged_command(start_command)
         self.send_command(start_command)
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index b4b425db39..2f86de1903 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -28,6 +28,7 @@
 from framework.exception import InteractiveCommandExecutionError
 from framework.parser import ParserFn, TextParser
 from framework.settings import SETTINGS
+from framework.testbed_model.sut_node import EalParams
 from framework.utils import StrEnum
 
 from .interactive_shell import InteractiveShell
@@ -645,8 +646,14 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
         Also find the number of pci addresses which were allowed on the command line when the app
         was started.
         """
-        self._app_args += " -i --mask-event intr_lsc"
-        self.number_of_ports = self._app_args.count("-a ")
+        self._app_params += " -i --mask-event intr_lsc"
+
+        assert isinstance(self._app_params, EalParams)
+
+        self.number_of_ports = (
+            len(self._app_params.ports) if self._app_params.ports is not None else 0
+        )
+
         super()._start_application(get_privileged_command)
 
     def start(self, verify: bool = True) -> None:
diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
index d7f5d45826..4ad2924c93 100644
--- a/dts/framework/testbed_model/node.py
+++ b/dts/framework/testbed_model/node.py
@@ -2,6 +2,7 @@
 # Copyright(c) 2010-2014 Intel Corporation
 # Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2022-2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """Common functionality for node management.
 
@@ -19,6 +20,7 @@
 from framework.config import OS, NodeConfiguration, TestRunConfiguration
 from framework.exception import ConfigurationError
 from framework.logger import DTSLogger, get_dts_logger
+from framework.params import Params
 from framework.settings import SETTINGS
 
 from .cpu import (
@@ -143,7 +145,7 @@ def create_interactive_shell(
         shell_cls: Type[InteractiveShellType],
         timeout: float = SETTINGS.timeout,
         privileged: bool = False,
-        app_args: str = "",
+        app_params: Params = Params(),
     ) -> InteractiveShellType:
         """Factory for interactive session handlers.
 
@@ -166,7 +168,7 @@ def create_interactive_shell(
             shell_cls,
             timeout,
             privileged,
-            app_args,
+            app_params,
         )
 
     def filter_lcores(
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
index 34b0a9e749..3b5b55811e 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright(c) 2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """OS-aware remote session.
 
@@ -29,6 +30,7 @@
 
 from framework.config import Architecture, NodeConfiguration, NodeInfo
 from framework.logger import DTSLogger
+from framework.params import Params
 from framework.remote_session import (
     CommandResult,
     InteractiveRemoteSession,
@@ -128,7 +130,7 @@ def create_interactive_shell(
         shell_cls: Type[InteractiveShellType],
         timeout: float,
         privileged: bool,
-        app_args: str,
+        app_args: Params,
     ) -> InteractiveShellType:
         """Factory for interactive session handlers.
 
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index e2a520fede..1a631c2732 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -2,6 +2,7 @@
 # Copyright(c) 2010-2014 Intel Corporation
 # Copyright(c) 2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2023 University of New Hampshire
+# Copyright(c) 2024 Arm Limited
 
 """System under test (DPDK + hardware) node.
 
@@ -14,8 +15,9 @@
 import os
 import tarfile
 import time
+from dataclasses import dataclass, field
 from pathlib import PurePath
-from typing import Type
+from typing import Literal, Type
 
 from framework.config import (
     BuildTargetConfiguration,
@@ -24,6 +26,7 @@
     SutNodeConfiguration,
     TestRunConfiguration,
 )
+from framework.params import Params, Switch
 from framework.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
@@ -35,62 +38,42 @@
 from .virtual_device import VirtualDevice
 
 
-class EalParameters:
-    """The environment abstraction layer parameters.
-
-    The string representation can be created by converting the instance to a string.
-    """
+def _port_to_pci(port: Port) -> str:
+    return port.pci
 
-    def __init__(
-        self,
-        lcore_list: LogicalCoreList,
-        memory_channels: int,
-        prefix: str,
-        no_pci: bool,
-        vdevs: list[VirtualDevice],
-        ports: list[Port],
-        other_eal_param: str,
-    ):
-        """Initialize the parameters according to inputs.
-
-        Process the parameters into the format used on the command line.
 
-        Args:
-            lcore_list: The list of logical cores to use.
-            memory_channels: The number of memory channels to use.
-            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
-            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
-            vdevs: Virtual devices, e.g.::
+@dataclass(kw_only=True)
+class EalParams(Params):
+    """The environment abstraction layer parameters.
 
-                vdevs=[
-                    VirtualDevice('net_ring0'),
-                    VirtualDevice('net_ring1')
-                ]
-            ports: The list of ports to allow.
-            other_eal_param: user defined DPDK EAL parameters, e.g.:
+    Attributes:
+        lcore_list: The list of logical cores to use.
+        memory_channels: The number of memory channels to use.
+        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
+        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
+        vdevs: Virtual devices, e.g.::
+            vdevs=[
+                VirtualDevice('net_ring0'),
+                VirtualDevice('net_ring1')
+            ]
+        ports: The list of ports to allow.
+        other_eal_param: user defined DPDK EAL parameters, e.g.:
                 ``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._ports = " ".join(f"-a {port.pci}" for port in ports)
-        self._other_eal_param = other_eal_param
-
-    def __str__(self) -> str:
-        """Create the EAL string."""
-        return (
-            f"{self._lcore_list} "
-            f"{self._memory_channels} "
-            f"{self._prefix} "
-            f"{self._no_pci} "
-            f"{self._vdevs} "
-            f"{self._ports} "
-            f"{self._other_eal_param}"
-        )
+    """
+
+    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
+    memory_channels: int = field(metadata=Params.short("n"))
+    prefix: str = field(metadata=Params.long("file-prefix"))
+    no_pci: Switch
+    vdevs: list[VirtualDevice] | None = field(
+        default=None, metadata=Params.multiple() | Params.long("vdev")
+    )
+    ports: list[Port] | None = field(
+        default=None,
+        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
+    )
+    other_eal_param: Params | None = None
+    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
 
 
 class SutNode(Node):
@@ -373,11 +356,11 @@ def create_eal_parameters(
         ascending_cores: bool = True,
         prefix: str = "dpdk",
         append_prefix_timestamp: bool = True,
-        no_pci: bool = False,
+        no_pci: Switch = None,
         vdevs: list[VirtualDevice] | None = None,
         ports: list[Port] | None = None,
         other_eal_param: str = "",
-    ) -> "EalParameters":
+    ) -> EalParams:
         """Compose the EAL parameters.
 
         Process the list of cores and the DPDK prefix and pass that along with
@@ -416,24 +399,21 @@ def create_eal_parameters(
         if prefix:
             self._dpdk_prefix_list.append(prefix)
 
-        if vdevs is None:
-            vdevs = []
-
         if ports is None:
             ports = self.ports
 
-        return EalParameters(
+        return EalParams(
             lcore_list=lcore_list,
             memory_channels=self.config.memory_channels,
             prefix=prefix,
             no_pci=no_pci,
             vdevs=vdevs,
             ports=ports,
-            other_eal_param=other_eal_param,
+            other_eal_param=Params.from_str(other_eal_param),
         )
 
     def run_dpdk_app(
-        self, app_path: PurePath, eal_args: "EalParameters", timeout: float = 30
+        self, app_path: PurePath, eal_params: EalParams, timeout: float = 30
     ) -> CommandResult:
         """Run DPDK application on the remote node.
 
@@ -442,14 +422,14 @@ def run_dpdk_app(
 
         Args:
             app_path: The remote path to the DPDK application.
-            eal_args: EAL parameters to run the DPDK application with.
+            eal_params: EAL parameters to run the DPDK application with.
             timeout: Wait at most this long in seconds for `command` execution to complete.
 
         Returns:
             The result of the DPDK app execution.
         """
         return self.main_session.send_command(
-            f"{app_path} {eal_args}", timeout, privileged=True, verify=True
+            f"{app_path} {eal_params}", timeout, privileged=True, verify=True
         )
 
     def configure_ipv4_forwarding(self, enable: bool) -> None:
@@ -465,8 +445,8 @@ def create_interactive_shell(
         shell_cls: Type[InteractiveShellType],
         timeout: float = SETTINGS.timeout,
         privileged: bool = False,
-        app_parameters: str = "",
-        eal_parameters: EalParameters | None = None,
+        app_params: Params = Params(),
+        eal_params: EalParams | None = None,
     ) -> InteractiveShellType:
         """Extend the factory for interactive session handlers.
 
@@ -482,26 +462,26 @@ def create_interactive_shell(
                 reading from the buffer and don't receive any data within the timeout
                 it will throw an error.
             privileged: Whether to run the shell with administrative privileges.
-            eal_parameters: List of EAL parameters to use to launch the app. If this
+            app_params: The parameters to be passed to the application.
+            eal_params: List of EAL parameters to use to launch the app. If this
                 isn't provided or an empty string is passed, it will default to calling
                 :meth:`create_eal_parameters`.
-            app_parameters: Additional arguments to pass into the application on the
-                command-line.
 
         Returns:
             An instance of the desired interactive application shell.
         """
         # We need to append the build directory and add EAL parameters for DPDK apps
         if shell_cls.dpdk_app:
-            if not eal_parameters:
-                eal_parameters = self.create_eal_parameters()
-            app_parameters = f"{eal_parameters} -- {app_parameters}"
+            if eal_params is None:
+                eal_params = self.create_eal_parameters()
+            eal_params.append_str(str(app_params))
+            app_params = eal_params
 
             shell_cls.path = self.main_session.join_remote_path(
                 self.remote_dpdk_build_dir, shell_cls.path
             )
 
-        return super().create_interactive_shell(shell_cls, timeout, privileged, app_parameters)
+        return super().create_interactive_shell(shell_cls, timeout, privileged, app_params)
 
     def bind_ports_to_driver(self, for_dpdk: bool = True) -> None:
         """Bind all ports on the SUT to a driver.
diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index 29a30c0b53..9338c35651 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -22,6 +22,7 @@
 from scapy.packet import Raw  # type: ignore[import-untyped]
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
+from framework.params import Params
 from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
 from framework.test_suite import TestSuite
 
@@ -103,7 +104,7 @@ def pmd_scatter(self, mbsize: int) -> None:
         """
         testpmd = self.sut_node.create_interactive_shell(
             TestPmdShell,
-            app_parameters=(
+            app_params=Params.from_str(
                 "--mbcache=200 "
                 f"--mbuf-size={mbsize} "
                 "--max-pkt-len=9000 "
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v7 3/8] dts: refactor EalParams
  2024-06-19 14:02 ` [PATCH v7 0/8] dts: add testpmd params Luca Vizzarro
  2024-06-19 14:02   ` [PATCH v7 1/8] dts: add params manipulation module Luca Vizzarro
  2024-06-19 14:02   ` [PATCH v7 2/8] dts: use Params for interactive shells Luca Vizzarro
@ 2024-06-19 14:03   ` Luca Vizzarro
  2024-06-19 14:03   ` [PATCH v7 4/8] dts: remove module-wide imports Luca Vizzarro
                     ` (5 subsequent siblings)
  8 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-19 14:03 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Move EalParams to its own module to avoid circular dependencies.
Also the majority of the attributes are now optional.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 dts/framework/params/eal.py                   | 50 +++++++++++++++++++
 dts/framework/remote_session/testpmd_shell.py |  2 +-
 dts/framework/testbed_model/sut_node.py       | 42 +---------------
 3 files changed, 53 insertions(+), 41 deletions(-)
 create mode 100644 dts/framework/params/eal.py

diff --git a/dts/framework/params/eal.py b/dts/framework/params/eal.py
new file mode 100644
index 0000000000..bbdbc8f334
--- /dev/null
+++ b/dts/framework/params/eal.py
@@ -0,0 +1,50 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Module representing the DPDK EAL-related parameters."""
+
+from dataclasses import dataclass, field
+from typing import Literal
+
+from framework.params import Params, Switch
+from framework.testbed_model.cpu import LogicalCoreList
+from framework.testbed_model.port import Port
+from framework.testbed_model.virtual_device import VirtualDevice
+
+
+def _port_to_pci(port: Port) -> str:
+    return port.pci
+
+
+@dataclass(kw_only=True)
+class EalParams(Params):
+    """The environment abstraction layer parameters.
+
+    Attributes:
+        lcore_list: The list of logical cores to use.
+        memory_channels: The number of memory channels to use.
+        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
+        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
+        vdevs: Virtual devices, e.g.::
+            vdevs=[
+                VirtualDevice('net_ring0'),
+                VirtualDevice('net_ring1')
+            ]
+        ports: The list of ports to allow.
+        other_eal_param: user defined DPDK EAL parameters, e.g.:
+                ``other_eal_param='--single-file-segments'``
+    """
+
+    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
+    memory_channels: int = field(metadata=Params.short("n"))
+    prefix: str = field(metadata=Params.long("file-prefix"))
+    no_pci: Switch = None
+    vdevs: list[VirtualDevice] | None = field(
+        default=None, metadata=Params.multiple() | Params.long("vdev")
+    )
+    ports: list[Port] | None = field(
+        default=None,
+        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
+    )
+    other_eal_param: Params | None = None
+    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index 2f86de1903..e71cda406f 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -26,9 +26,9 @@
 from typing_extensions import Self
 
 from framework.exception import InteractiveCommandExecutionError
+from framework.params.eal import EalParams
 from framework.parser import ParserFn, TextParser
 from framework.settings import SETTINGS
-from framework.testbed_model.sut_node import EalParams
 from framework.utils import StrEnum
 
 from .interactive_shell import InteractiveShell
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index 1a631c2732..b834374c21 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -15,9 +15,8 @@
 import os
 import tarfile
 import time
-from dataclasses import dataclass, field
 from pathlib import PurePath
-from typing import Literal, Type
+from typing import Type
 
 from framework.config import (
     BuildTargetConfiguration,
@@ -27,6 +26,7 @@
     TestRunConfiguration,
 )
 from framework.params import Params, Switch
+from framework.params.eal import EalParams
 from framework.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
@@ -38,44 +38,6 @@
 from .virtual_device import VirtualDevice
 
 
-def _port_to_pci(port: Port) -> str:
-    return port.pci
-
-
-@dataclass(kw_only=True)
-class EalParams(Params):
-    """The environment abstraction layer parameters.
-
-    Attributes:
-        lcore_list: The list of logical cores to use.
-        memory_channels: The number of memory channels to use.
-        prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix="vf"``.
-        no_pci: Switch to disable PCI bus, e.g.: ``no_pci=True``.
-        vdevs: Virtual devices, e.g.::
-            vdevs=[
-                VirtualDevice('net_ring0'),
-                VirtualDevice('net_ring1')
-            ]
-        ports: The list of ports to allow.
-        other_eal_param: user defined DPDK EAL parameters, e.g.:
-                ``other_eal_param='--single-file-segments'``
-    """
-
-    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
-    memory_channels: int = field(metadata=Params.short("n"))
-    prefix: str = field(metadata=Params.long("file-prefix"))
-    no_pci: Switch
-    vdevs: list[VirtualDevice] | None = field(
-        default=None, metadata=Params.multiple() | Params.long("vdev")
-    )
-    ports: list[Port] | None = field(
-        default=None,
-        metadata=Params.convert_value(_port_to_pci) | Params.multiple() | Params.short("a"),
-    )
-    other_eal_param: Params | None = None
-    _separator: Literal[True] = field(default=True, init=False, metadata=Params.short("-"))
-
-
 class SutNode(Node):
     """The system under test node.
 
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v7 4/8] dts: remove module-wide imports
  2024-06-19 14:02 ` [PATCH v7 0/8] dts: add testpmd params Luca Vizzarro
                     ` (2 preceding siblings ...)
  2024-06-19 14:03   ` [PATCH v7 3/8] dts: refactor EalParams Luca Vizzarro
@ 2024-06-19 14:03   ` Luca Vizzarro
  2024-06-19 14:03   ` [PATCH v7 5/8] dts: add testpmd shell params Luca Vizzarro
                     ` (4 subsequent siblings)
  8 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-19 14:03 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Remove the imports in the testbed_model and remote_session modules init
file, to avoid the initialisation of unneeded modules, thus removing or
limiting the risk of circular dependencies.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 dts/framework/remote_session/__init__.py                 | 7 +------
 dts/framework/runner.py                                  | 4 +++-
 dts/framework/test_suite.py                              | 9 +++++++--
 dts/framework/testbed_model/__init__.py                  | 9 ---------
 dts/framework/testbed_model/os_session.py                | 4 ++--
 dts/framework/testbed_model/sut_node.py                  | 2 +-
 dts/framework/testbed_model/tg_node.py                   | 9 ++++-----
 .../testbed_model/traffic_generator/__init__.py          | 7 +------
 dts/framework/testbed_model/traffic_generator/scapy.py   | 2 +-
 dts/tests/TestSuite_hello_world.py                       | 2 +-
 dts/tests/TestSuite_smoke_tests.py                       | 2 +-
 11 files changed, 22 insertions(+), 35 deletions(-)

diff --git a/dts/framework/remote_session/__init__.py b/dts/framework/remote_session/__init__.py
index 1910c81c3c..0668e9c884 100644
--- a/dts/framework/remote_session/__init__.py
+++ b/dts/framework/remote_session/__init__.py
@@ -12,17 +12,12 @@
 allowing it to send and receive data within that particular shell.
 """
 
-# pylama:ignore=W0611
-
 from framework.config import NodeConfiguration
 from framework.logger import DTSLogger
 
 from .interactive_remote_session import InteractiveRemoteSession
-from .interactive_shell import InteractiveShell
-from .python_shell import PythonShell
-from .remote_session import CommandResult, RemoteSession
+from .remote_session import RemoteSession
 from .ssh_session import SSHSession
-from .testpmd_shell import TestPmdShell
 
 
 def create_remote_session(
diff --git a/dts/framework/runner.py b/dts/framework/runner.py
index 565e581310..6b6f6a05f5 100644
--- a/dts/framework/runner.py
+++ b/dts/framework/runner.py
@@ -26,6 +26,9 @@
 from types import FunctionType
 from typing import Iterable, Sequence
 
+from framework.testbed_model.sut_node import SutNode
+from framework.testbed_model.tg_node import TGNode
+
 from .config import (
     BuildTargetConfiguration,
     Configuration,
@@ -51,7 +54,6 @@
     TestSuiteWithCases,
 )
 from .test_suite import TestSuite
-from .testbed_model import SutNode, TGNode
 
 
 class DTSRunner:
diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py
index b9f8daab1a..694b2eba65 100644
--- a/dts/framework/test_suite.py
+++ b/dts/framework/test_suite.py
@@ -20,10 +20,15 @@
 from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
 from scapy.packet import Packet, Padding  # type: ignore[import-untyped]
 
+from framework.testbed_model.port import Port, PortLink
+from framework.testbed_model.sut_node import SutNode
+from framework.testbed_model.tg_node import TGNode
+from framework.testbed_model.traffic_generator.capturing_traffic_generator import (
+    PacketFilteringConfig,
+)
+
 from .exception import TestCaseVerifyError
 from .logger import DTSLogger, get_dts_logger
-from .testbed_model import Port, PortLink, SutNode, TGNode
-from .testbed_model.traffic_generator import PacketFilteringConfig
 from .utils import get_packet_summaries
 
 
diff --git a/dts/framework/testbed_model/__init__.py b/dts/framework/testbed_model/__init__.py
index 6086512ca2..e3edd4d811 100644
--- a/dts/framework/testbed_model/__init__.py
+++ b/dts/framework/testbed_model/__init__.py
@@ -17,12 +17,3 @@
 DTS needs to be able to connect to nodes and understand some of the hardware present on these nodes
 to properly build and test DPDK.
 """
-
-# pylama:ignore=W0611
-
-from .cpu import LogicalCoreCount, LogicalCoreCountFilter, LogicalCoreList
-from .node import Node
-from .port import Port, PortLink
-from .sut_node import SutNode
-from .tg_node import TGNode
-from .virtual_device import VirtualDevice
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
index 3b5b55811e..84000931ff 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -32,13 +32,13 @@
 from framework.logger import DTSLogger
 from framework.params import Params
 from framework.remote_session import (
-    CommandResult,
     InteractiveRemoteSession,
-    InteractiveShell,
     RemoteSession,
     create_interactive_session,
     create_remote_session,
 )
+from framework.remote_session.interactive_shell import InteractiveShell
+from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
 
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index b834374c21..9bb4fc78b1 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -27,7 +27,7 @@
 )
 from framework.params import Params, Switch
 from framework.params.eal import EalParams
-from framework.remote_session import CommandResult
+from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
 
diff --git a/dts/framework/testbed_model/tg_node.py b/dts/framework/testbed_model/tg_node.py
index 9dae56ed9c..4ee326e99c 100644
--- a/dts/framework/testbed_model/tg_node.py
+++ b/dts/framework/testbed_model/tg_node.py
@@ -12,14 +12,13 @@
 from scapy.packet import Packet  # type: ignore[import-untyped]
 
 from framework.config import TGNodeConfiguration
+from framework.testbed_model.traffic_generator.capturing_traffic_generator import (
+    PacketFilteringConfig,
+)
 
 from .node import Node
 from .port import Port
-from .traffic_generator import (
-    CapturingTrafficGenerator,
-    PacketFilteringConfig,
-    create_traffic_generator,
-)
+from .traffic_generator import CapturingTrafficGenerator, create_traffic_generator
 
 
 class TGNode(Node):
diff --git a/dts/framework/testbed_model/traffic_generator/__init__.py b/dts/framework/testbed_model/traffic_generator/__init__.py
index 03e57a77fc..6dac86a224 100644
--- a/dts/framework/testbed_model/traffic_generator/__init__.py
+++ b/dts/framework/testbed_model/traffic_generator/__init__.py
@@ -14,16 +14,11 @@
 and a capturing traffic generator is required.
 """
 
-# pylama:ignore=W0611
-
 from framework.config import ScapyTrafficGeneratorConfig, TrafficGeneratorConfig
 from framework.exception import ConfigurationError
 from framework.testbed_model.node import Node
 
-from .capturing_traffic_generator import (
-    CapturingTrafficGenerator,
-    PacketFilteringConfig,
-)
+from .capturing_traffic_generator import CapturingTrafficGenerator
 from .scapy import ScapyTrafficGenerator
 
 
diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py
index ed5467d825..7bc1c2cc08 100644
--- a/dts/framework/testbed_model/traffic_generator/scapy.py
+++ b/dts/framework/testbed_model/traffic_generator/scapy.py
@@ -25,7 +25,7 @@
 from scapy.packet import Packet  # type: ignore[import-untyped]
 
 from framework.config import OS, ScapyTrafficGeneratorConfig
-from framework.remote_session import PythonShell
+from framework.remote_session.python_shell import PythonShell
 from framework.settings import SETTINGS
 from framework.testbed_model.node import Node
 from framework.testbed_model.port import Port
diff --git a/dts/tests/TestSuite_hello_world.py b/dts/tests/TestSuite_hello_world.py
index fd7ff1534d..0d6995f260 100644
--- a/dts/tests/TestSuite_hello_world.py
+++ b/dts/tests/TestSuite_hello_world.py
@@ -8,7 +8,7 @@
 """
 
 from framework.test_suite import TestSuite
-from framework.testbed_model import (
+from framework.testbed_model.cpu import (
     LogicalCoreCount,
     LogicalCoreCountFilter,
     LogicalCoreList,
diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py
index a553e89662..ca678f662d 100644
--- a/dts/tests/TestSuite_smoke_tests.py
+++ b/dts/tests/TestSuite_smoke_tests.py
@@ -15,7 +15,7 @@
 import re
 
 from framework.config import PortConfig
-from framework.remote_session import TestPmdShell
+from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.settings import SETTINGS
 from framework.test_suite import TestSuite
 from framework.utils import REGEX_FOR_PCI_ADDRESS
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v7 5/8] dts: add testpmd shell params
  2024-06-19 14:02 ` [PATCH v7 0/8] dts: add testpmd params Luca Vizzarro
                     ` (3 preceding siblings ...)
  2024-06-19 14:03   ` [PATCH v7 4/8] dts: remove module-wide imports Luca Vizzarro
@ 2024-06-19 14:03   ` Luca Vizzarro
  2024-06-19 14:03   ` [PATCH v7 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
                     ` (3 subsequent siblings)
  8 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-19 14:03 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Implement all the testpmd shell parameters into a data structure.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 dts/framework/params/testpmd.py               | 607 ++++++++++++++++++
 dts/framework/remote_session/testpmd_shell.py |  39 +-
 dts/tests/TestSuite_pmd_buffer_scatter.py     |   5 +-
 3 files changed, 613 insertions(+), 38 deletions(-)
 create mode 100644 dts/framework/params/testpmd.py

diff --git a/dts/framework/params/testpmd.py b/dts/framework/params/testpmd.py
new file mode 100644
index 0000000000..1913bd0fa2
--- /dev/null
+++ b/dts/framework/params/testpmd.py
@@ -0,0 +1,607 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Module containing all the TestPmd-related parameter classes."""
+
+from dataclasses import dataclass, field
+from enum import EnumMeta, Flag, auto, unique
+from pathlib import PurePath
+from typing import Literal, NamedTuple
+
+from framework.params import (
+    Params,
+    Switch,
+    YesNoSwitch,
+    bracketed,
+    comma_separated,
+    hex_from_flag_value,
+    modify_str,
+    str_from_flag_value,
+)
+from framework.params.eal import EalParams
+from framework.utils import StrEnum
+
+
+class PortTopology(StrEnum):
+    """Enum representing the port topology."""
+
+    #: In paired mode, the forwarding is between pairs of ports, e.g.: (0,1), (2,3), (4,5).
+    paired = auto()
+
+    #: In chained mode, the forwarding is to the next available port in the port mask, e.g.:
+    #: (0,1), (1,2), (2,0).
+    #:
+    #: The ordering of the ports can be changed using the portlist testpmd runtime function.
+    chained = auto()
+
+    #: In loop mode, ingress traffic is simply transmitted back on the same interface.
+    loop = auto()
+
+
+@modify_str(comma_separated, bracketed)
+class PortNUMAConfig(NamedTuple):
+    """DPDK port to NUMA socket association tuple."""
+
+    #:
+    port: int
+    #:
+    socket: int
+
+
+@modify_str(str_from_flag_value)
+@unique
+class FlowDirection(Flag):
+    """Flag indicating the direction of the flow.
+
+    A bi-directional flow can be specified with the pipe:
+
+    >>> TestPmdFlowDirection.RX | TestPmdFlowDirection.TX
+    <TestPmdFlowDirection.TX|RX: 3>
+    """
+
+    #:
+    RX = 1 << 0
+    #:
+    TX = 1 << 1
+
+
+@modify_str(comma_separated, bracketed)
+class RingNUMAConfig(NamedTuple):
+    """Tuple associating DPDK port, direction of the flow and NUMA socket."""
+
+    #:
+    port: int
+    #:
+    direction: FlowDirection
+    #:
+    socket: int
+
+
+@modify_str(comma_separated)
+class EthPeer(NamedTuple):
+    """Tuple associating a MAC address to the specified DPDK port."""
+
+    #:
+    port_no: int
+    #:
+    mac_address: str
+
+
+@modify_str(comma_separated)
+class TxIPAddrPair(NamedTuple):
+    """Tuple specifying the source and destination IPs for the packets."""
+
+    #:
+    source_ip: str
+    #:
+    dest_ip: str
+
+
+@modify_str(comma_separated)
+class TxUDPPortPair(NamedTuple):
+    """Tuple specifying the UDP source and destination ports for the packets.
+
+    If leaving ``dest_port`` unspecified, ``source_port`` will be used for
+    the destination port as well.
+    """
+
+    #:
+    source_port: int
+    #:
+    dest_port: int | None = None
+
+
+@dataclass
+class DisableRSS(Params):
+    """Disables RSS (Receive Side Scaling)."""
+
+    _disable_rss: Literal[True] = field(
+        default=True, init=False, metadata=Params.long("disable-rss")
+    )
+
+
+@dataclass
+class SetRSSIPOnly(Params):
+    """Sets RSS (Receive Side Scaling) functions for IPv4/IPv6 only."""
+
+    _rss_ip: Literal[True] = field(default=True, init=False, metadata=Params.long("rss-ip"))
+
+
+@dataclass
+class SetRSSUDP(Params):
+    """Sets RSS (Receive Side Scaling) functions for IPv4/IPv6 and UDP."""
+
+    _rss_udp: Literal[True] = field(default=True, init=False, metadata=Params.long("rss-udp"))
+
+
+class RSSSetting(EnumMeta):
+    """Enum representing a RSS setting. Each property is a class that needs to be initialised."""
+
+    #:
+    Disabled = DisableRSS
+    #:
+    SetIPOnly = SetRSSIPOnly
+    #:
+    SetUDP = SetRSSUDP
+
+
+class SimpleForwardingModes(StrEnum):
+    r"""The supported packet forwarding modes for :class:`~TestPmdShell`\s."""
+
+    #:
+    io = auto()
+    #:
+    mac = auto()
+    #:
+    macswap = auto()
+    #:
+    rxonly = auto()
+    #:
+    csum = auto()
+    #:
+    icmpecho = auto()
+    #:
+    ieee1588 = auto()
+    #:
+    fivetswap = "5tswap"
+    #:
+    shared_rxq = "shared-rxq"
+    #:
+    recycle_mbufs = auto()
+
+
+@dataclass(kw_only=True)
+class TXOnlyForwardingMode(Params):
+    """Sets a TX-Only forwarding mode.
+
+    Attributes:
+        multi_flow: Generates multiple flows if set to True.
+        segments_length: Sets TX segment sizes or total packet length.
+    """
+
+    _forward_mode: Literal["txonly"] = field(
+        default="txonly", init=False, metadata=Params.long("forward-mode")
+    )
+    multi_flow: Switch = field(default=None, metadata=Params.long("txonly-multi-flow"))
+    segments_length: list[int] | None = field(
+        default=None, metadata=Params.long("txpkts") | Params.convert_value(comma_separated)
+    )
+
+
+@dataclass(kw_only=True)
+class FlowGenForwardingMode(Params):
+    """Sets a flowgen forwarding mode.
+
+    Attributes:
+        clones: Set the number of each packet clones to be sent. Sending clones reduces host CPU
+                load on creating packets and may help in testing extreme speeds or maxing out
+                Tx packet performance. N should be not zero, but less than ‘burst’ parameter.
+        flows: Set the number of flows to be generated, where 1 <= N <= INT32_MAX.
+        segments_length: Set TX segment sizes or total packet length.
+    """
+
+    _forward_mode: Literal["flowgen"] = field(
+        default="flowgen", init=False, metadata=Params.long("forward-mode")
+    )
+    clones: int | None = field(default=None, metadata=Params.long("flowgen-clones"))
+    flows: int | None = field(default=None, metadata=Params.long("flowgen-flows"))
+    segments_length: list[int] | None = field(
+        default=None, metadata=Params.long("txpkts") | Params.convert_value(comma_separated)
+    )
+
+
+@dataclass(kw_only=True)
+class NoisyForwardingMode(Params):
+    """Sets a noisy forwarding mode.
+
+    Attributes:
+        forward_mode: Set the noisy VNF forwarding mode.
+        tx_sw_buffer_size: Set the maximum number of elements of the FIFO queue to be created for
+                           buffering packets.
+        tx_sw_buffer_flushtime: Set the time before packets in the FIFO queue are flushed.
+        lkup_memory: Set the size of the noisy neighbor simulation memory buffer in MB to N.
+        lkup_num_reads: Set the size of the noisy neighbor simulation memory buffer in MB to N.
+        lkup_num_writes: Set the number of writes to be done in noisy neighbor simulation
+                         memory buffer to N.
+        lkup_num_reads_writes: Set the number of r/w accesses to be done in noisy neighbor
+                               simulation memory buffer to N.
+    """
+
+    _forward_mode: Literal["noisy"] = field(
+        default="noisy", init=False, metadata=Params.long("forward-mode")
+    )
+    forward_mode: (
+        Literal[
+            SimpleForwardingModes.io,
+            SimpleForwardingModes.mac,
+            SimpleForwardingModes.macswap,
+            SimpleForwardingModes.fivetswap,
+        ]
+        | None
+    ) = field(default=SimpleForwardingModes.io, metadata=Params.long("noisy-forward-mode"))
+    tx_sw_buffer_size: int | None = field(
+        default=None, metadata=Params.long("noisy-tx-sw-buffer-size")
+    )
+    tx_sw_buffer_flushtime: int | None = field(
+        default=None, metadata=Params.long("noisy-tx-sw-buffer-flushtime")
+    )
+    lkup_memory: int | None = field(default=None, metadata=Params.long("noisy-lkup-memory"))
+    lkup_num_reads: int | None = field(default=None, metadata=Params.long("noisy-lkup-num-reads"))
+    lkup_num_writes: int | None = field(default=None, metadata=Params.long("noisy-lkup-num-writes"))
+    lkup_num_reads_writes: int | None = field(
+        default=None, metadata=Params.long("noisy-lkup-num-reads-writes")
+    )
+
+
+@modify_str(hex_from_flag_value)
+@unique
+class HairpinMode(Flag):
+    """Flag representing the hairpin mode."""
+
+    #: Two hairpin ports loop.
+    TWO_PORTS_LOOP = 1 << 0
+    #: Two hairpin ports paired.
+    TWO_PORTS_PAIRED = 1 << 1
+    #: Explicit Tx flow rule.
+    EXPLICIT_TX_FLOW = 1 << 4
+    #: Force memory settings of hairpin RX queue.
+    FORCE_RX_QUEUE_MEM_SETTINGS = 1 << 8
+    #: Force memory settings of hairpin TX queue.
+    FORCE_TX_QUEUE_MEM_SETTINGS = 1 << 9
+    #: Hairpin RX queues will use locked device memory.
+    RX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 12
+    #: Hairpin RX queues will use RTE memory.
+    RX_QUEUE_USE_RTE_MEMORY = 1 << 13
+    #: Hairpin TX queues will use locked device memory.
+    TX_QUEUE_USE_LOCKED_DEVICE_MEMORY = 1 << 16
+    #: Hairpin TX queues will use RTE memory.
+    TX_QUEUE_USE_RTE_MEMORY = 1 << 18
+
+
+@dataclass(kw_only=True)
+class RXRingParams(Params):
+    """Sets the RX ring parameters.
+
+    Attributes:
+        descriptors: Set the number of descriptors in the RX rings to N, where N > 0.
+        prefetch_threshold: Set the prefetch threshold register of RX rings to N, where N >= 0.
+        host_threshold: Set the host threshold register of RX rings to N, where N >= 0.
+        write_back_threshold: Set the write-back threshold register of RX rings to N, where N >= 0.
+        free_threshold: Set the free threshold of RX descriptors to N,
+                        where 0 <= N < value of ``-–rxd``.
+    """
+
+    descriptors: int | None = field(default=None, metadata=Params.long("rxd"))
+    prefetch_threshold: int | None = field(default=None, metadata=Params.long("rxpt"))
+    host_threshold: int | None = field(default=None, metadata=Params.long("rxht"))
+    write_back_threshold: int | None = field(default=None, metadata=Params.long("rxwt"))
+    free_threshold: int | None = field(default=None, metadata=Params.long("rxfreet"))
+
+
+@modify_str(hex_from_flag_value)
+@unique
+class RXMultiQueueMode(Flag):
+    """Flag representing the RX multi-queue mode."""
+
+    #:
+    RSS = 1 << 0
+    #:
+    DCB = 1 << 1
+    #:
+    VMDQ = 1 << 2
+
+
+@dataclass(kw_only=True)
+class TXRingParams(Params):
+    """Sets the TX ring parameters.
+
+    Attributes:
+        descriptors: Set the number of descriptors in the TX rings to N, where N > 0.
+        rs_bit_threshold: Set the transmit RS bit threshold of TX rings to N,
+                          where 0 <= N <= value of ``--txd``.
+        prefetch_threshold: Set the prefetch threshold register of TX rings to N, where N >= 0.
+        host_threshold: Set the host threshold register of TX rings to N, where N >= 0.
+        write_back_threshold: Set the write-back threshold register of TX rings to N, where N >= 0.
+        free_threshold: Set the transmit free threshold of TX rings to N,
+                        where 0 <= N <= value of ``--txd``.
+    """
+
+    descriptors: int | None = field(default=None, metadata=Params.long("txd"))
+    rs_bit_threshold: int | None = field(default=None, metadata=Params.long("txrst"))
+    prefetch_threshold: int | None = field(default=None, metadata=Params.long("txpt"))
+    host_threshold: int | None = field(default=None, metadata=Params.long("txht"))
+    write_back_threshold: int | None = field(default=None, metadata=Params.long("txwt"))
+    free_threshold: int | None = field(default=None, metadata=Params.long("txfreet"))
+
+
+class Event(StrEnum):
+    """Enum representing a testpmd event."""
+
+    #:
+    unknown = auto()
+    #:
+    queue_state = auto()
+    #:
+    vf_mbox = auto()
+    #:
+    macsec = auto()
+    #:
+    intr_lsc = auto()
+    #:
+    intr_rmv = auto()
+    #:
+    intr_reset = auto()
+    #:
+    dev_probed = auto()
+    #:
+    dev_released = auto()
+    #:
+    flow_aged = auto()
+    #:
+    err_recovering = auto()
+    #:
+    recovery_success = auto()
+    #:
+    recovery_failed = auto()
+    #:
+    all = auto()
+
+
+class SimpleMempoolAllocationMode(StrEnum):
+    """Enum representing simple mempool allocation modes."""
+
+    #: Create and populate mempool using native DPDK memory.
+    native = auto()
+    #: Create and populate mempool using externally and anonymously allocated area.
+    xmem = auto()
+    #: Create and populate mempool using externally and anonymously allocated hugepage area.
+    xmemhuge = auto()
+
+
+@dataclass(kw_only=True)
+class AnonMempoolAllocationMode(Params):
+    """Create mempool using native DPDK memory, but populate using anonymous memory.
+
+    Attributes:
+        no_iova_contig: Enables to create mempool which is not IOVA contiguous.
+    """
+
+    _mp_alloc: Literal["anon"] = field(default="anon", init=False, metadata=Params.long("mp-alloc"))
+    no_iova_contig: Switch = None
+
+
+@dataclass(slots=True, kw_only=True)
+class TestPmdParams(EalParams):
+    """The testpmd shell parameters.
+
+    Attributes:
+        interactive_mode: Run testpmd in interactive mode.
+        auto_start: Start forwarding on initialization.
+        tx_first: Start forwarding, after sending a burst of packets first.
+        stats_period: Display statistics every ``PERIOD`` seconds, if interactive mode is disabled.
+                      The default value is 0, which means that the statistics will not be displayed.
+
+                      .. note:: This flag should be used only in non-interactive mode.
+        display_xstats: Display comma-separated list of extended statistics every ``PERIOD`` seconds
+                        as specified in ``--stats-period`` or when used with interactive commands
+                        that show Rx/Tx statistics (i.e. ‘show port stats’).
+        nb_cores: Set the number of forwarding cores, where 1 <= N <= “number of cores” or
+                  ``RTE_MAX_LCORE`` from the configuration file.
+        coremask: Set the bitmask of the cores running the packet forwarding test. The main
+                  lcore is reserved for command line parsing only and cannot be masked on for packet
+                  forwarding.
+        nb_ports: Set the number of forwarding ports, where 1 <= N <= “number of ports” on the board
+                  or ``RTE_MAX_ETHPORTS`` from the configuration file. The default value is the
+                  number of ports on the board.
+        port_topology: Set port topology, where mode is paired (the default), chained or loop.
+        portmask: Set the bitmask of the ports used by the packet forwarding test.
+        portlist: Set the forwarding ports based on the user input used by the packet forwarding
+                  test. ‘-‘ denotes a range of ports to set including the two specified port IDs ‘,’
+                  separates multiple port values. Possible examples like –portlist=0,1 or
+                  –portlist=0-2 or –portlist=0,1-2 etc.
+        numa: Enable/disable NUMA-aware allocation of RX/TX rings and of RX memory buffers (mbufs).
+        socket_num: Set the socket from which all memory is allocated in NUMA mode, where
+                    0 <= N < number of sockets on the board.
+        port_numa_config: Specify the socket on which the memory pool to be used by the port will be
+                          allocated.
+        ring_numa_config: Specify the socket on which the TX/RX rings for the port will be
+                          allocated. Where flag is 1 for RX, 2 for TX, and 3 for RX and TX.
+        total_num_mbufs: Set the number of mbufs to be allocated in the mbuf pools, where N > 1024.
+        mbuf_size: Set the data size of the mbufs used to N bytes, where N < 65536.
+                   If multiple mbuf-size values are specified the extra memory pools will be created
+                   for allocating mbufs to receive packets with buffer splitting features.
+        mbcache: Set the cache of mbuf memory pools to N, where 0 <= N <= 512.
+        max_pkt_len: Set the maximum packet size to N bytes, where N >= 64.
+        eth_peers_configfile: Use a configuration file containing the Ethernet addresses of
+                              the peer ports.
+        eth_peer: Set the MAC address XX:XX:XX:XX:XX:XX of the peer port N,
+                  where 0 <= N < RTE_MAX_ETHPORTS.
+        tx_ip: Set the source and destination IP address used when doing transmit only test.
+               The defaults address values are source 198.18.0.1 and destination 198.18.0.2.
+               These are special purpose addresses reserved for benchmarking (RFC 5735).
+        tx_udp: Set the source and destination UDP port number for transmit test only test.
+                The default port is the port 9 which is defined for the discard protocol (RFC 863).
+        enable_lro: Enable large receive offload.
+        max_lro_pkt_size: Set the maximum LRO aggregated packet size to N bytes, where N >= 64.
+        disable_crc_strip: Disable hardware CRC stripping.
+        enable_scatter: Enable scatter (multi-segment) RX.
+        enable_hw_vlan: Enable hardware VLAN.
+        enable_hw_vlan_filter: Enable hardware VLAN filter.
+        enable_hw_vlan_strip: Enable hardware VLAN strip.
+        enable_hw_vlan_extend: Enable hardware VLAN extend.
+        enable_hw_qinq_strip: Enable hardware QINQ strip.
+        pkt_drop_enabled: Enable per-queue packet drop for packets with no descriptors.
+        rss: Receive Side Scaling setting.
+        forward_mode: Set the forwarding mode.
+        hairpin_mode: Set the hairpin port configuration.
+        hairpin_queues: Set the number of hairpin queues per port to N, where 1 <= N <= 65535.
+        burst: Set the number of packets per burst to N, where 1 <= N <= 512.
+        enable_rx_cksum: Enable hardware RX checksum offload.
+        rx_queues: Set the number of RX queues per port to N, where 1 <= N <= 65535.
+        rx_ring: Set the RX rings parameters.
+        no_flush_rx: Don’t flush the RX streams before starting forwarding. Used mainly with
+                     the PCAP PMD.
+        rx_segments_offsets: Set the offsets of packet segments on receiving
+                             if split feature is engaged.
+        rx_segments_length: Set the length of segments to scatter packets on receiving
+                            if split feature is engaged.
+        multi_rx_mempool: Enable multiple mbuf pools per Rx queue.
+        rx_shared_queue: Create queues in shared Rx queue mode if device supports. Shared Rx queues
+                         are grouped per X ports. X defaults to UINT32_MAX, implies all ports join
+                         share group 1. Forwarding engine “shared-rxq” should be used for shared Rx
+                         queues. This engine does Rx only and update stream statistics accordingly.
+        rx_offloads: Set the bitmask of RX queue offloads.
+        rx_mq_mode: Set the RX multi queue mode which can be enabled.
+        tx_queues: Set the number of TX queues per port to N, where 1 <= N <= 65535.
+        tx_ring: Set the TX rings params.
+        tx_offloads: Set the hexadecimal bitmask of TX queue offloads.
+        eth_link_speed: Set a forced link speed to the ethernet port. E.g. 1000 for 1Gbps.
+        disable_link_check: Disable check on link status when starting/stopping ports.
+        disable_device_start: Do not automatically start all ports. This allows testing
+                              configuration of rx and tx queues before device is started
+                              for the first time.
+        no_lsc_interrupt: Disable LSC interrupts for all ports, even those supporting it.
+        no_rmv_interrupt: Disable RMV interrupts for all ports, even those supporting it.
+        bitrate_stats: Set the logical core N to perform bitrate calculation.
+        latencystats: Set the logical core N to perform latency and jitter calculations.
+        print_events: Enable printing the occurrence of the designated events.
+                      Using :attr:`TestPmdEvent.ALL` will enable all of them.
+        mask_events: Disable printing the occurrence of the designated events.
+                     Using :attr:`TestPmdEvent.ALL` will disable all of them.
+        flow_isolate_all: Providing this parameter requests flow API isolated mode on all ports at
+                          initialization time. It ensures all traffic is received through the
+                          configured flow rules only (see flow command). Ports that do not support
+                          this mode are automatically discarded.
+        disable_flow_flush: Disable port flow flush when stopping port.
+                            This allows testing keep flow rules or shared flow objects across
+                            restart.
+        hot_plug: Enable device event monitor mechanism for hotplug.
+        vxlan_gpe_port: Set the UDP port number of tunnel VXLAN-GPE to N.
+        geneve_parsed_port: Set the UDP port number that is used for parsing the GENEVE protocol
+                            to N. HW may be configured with another tunnel Geneve port.
+        lock_all_memory: Enable/disable locking all memory. Disabled by default.
+        mempool_allocation_mode: Set mempool allocation mode.
+        record_core_cycles: Enable measurement of CPU cycles per packet.
+        record_burst_status: Enable display of RX and TX burst stats.
+    """
+
+    interactive_mode: Switch = field(default=True, metadata=Params.short("i"))
+    auto_start: Switch = field(default=None, metadata=Params.short("a"))
+    tx_first: Switch = None
+    stats_period: int | None = None
+    display_xstats: list[str] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    nb_cores: int | None = None
+    coremask: int | None = field(default=None, metadata=Params.convert_value(hex))
+    nb_ports: int | None = None
+    port_topology: PortTopology | None = PortTopology.paired
+    portmask: int | None = field(default=None, metadata=Params.convert_value(hex))
+    portlist: str | None = None  # TODO: can be ranges 0,1-3
+
+    numa: YesNoSwitch = None
+    socket_num: int | None = None
+    port_numa_config: list[PortNUMAConfig] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    ring_numa_config: list[RingNUMAConfig] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    total_num_mbufs: int | None = None
+    mbuf_size: list[int] | None = field(
+        default=None, metadata=Params.convert_value(comma_separated)
+    )
+    mbcache: int | None = None
+    max_pkt_len: int | None = None
+    eth_peers_configfile: PurePath | None = None
+    eth_peer: list[EthPeer] | None = field(default=None, metadata=Params.multiple())
+    tx_ip: TxIPAddrPair | None = None
+    tx_udp: TxUDPPortPair | None = None
+    enable_lro: Switch = None
+    max_lro_pkt_size: int | None = None
+    disable_crc_strip: Switch = None
+    enable_scatter: Switch = None
+    enable_hw_vlan: Switch = None
+    enable_hw_vlan_filter: Switch = None
+    enable_hw_vlan_strip: Switch = None
+    enable_hw_vlan_extend: Switch = None
+    enable_hw_qinq_strip: Switch = None
+    pkt_drop_enabled: Switch = field(default=None, metadata=Params.long("enable-drop-en"))
+    rss: RSSSetting | None = None
+    forward_mode: (
+        SimpleForwardingModes
+        | FlowGenForwardingMode
+        | TXOnlyForwardingMode
+        | NoisyForwardingMode
+        | None
+    ) = None
+    hairpin_mode: HairpinMode | None = None
+    hairpin_queues: int | None = field(default=None, metadata=Params.long("hairpinq"))
+    burst: int | None = None
+    enable_rx_cksum: Switch = None
+
+    rx_queues: int | None = field(default=None, metadata=Params.long("rxq"))
+    rx_ring: RXRingParams | None = None
+    no_flush_rx: Switch = None
+    rx_segments_offsets: list[int] | None = field(
+        default=None, metadata=Params.long("rxoffs") | Params.convert_value(comma_separated)
+    )
+    rx_segments_length: list[int] | None = field(
+        default=None, metadata=Params.long("rxpkts") | Params.convert_value(comma_separated)
+    )
+    multi_rx_mempool: Switch = None
+    rx_shared_queue: Switch | int = field(default=None, metadata=Params.long("rxq-share"))
+    rx_offloads: int | None = field(default=None, metadata=Params.convert_value(hex))
+    rx_mq_mode: RXMultiQueueMode | None = None
+
+    tx_queues: int | None = field(default=None, metadata=Params.long("txq"))
+    tx_ring: TXRingParams | None = None
+    tx_offloads: int | None = field(default=None, metadata=Params.convert_value(hex))
+
+    eth_link_speed: int | None = None
+    disable_link_check: Switch = None
+    disable_device_start: Switch = None
+    no_lsc_interrupt: Switch = None
+    no_rmv_interrupt: Switch = None
+    bitrate_stats: int | None = None
+    latencystats: int | None = None
+    print_events: list[Event] | None = field(
+        default=None, metadata=Params.multiple() | Params.long("print-event")
+    )
+    mask_events: list[Event] | None = field(
+        default_factory=lambda: [Event.intr_lsc],
+        metadata=Params.multiple() | Params.long("mask-event"),
+    )
+
+    flow_isolate_all: Switch = None
+    disable_flow_flush: Switch = None
+
+    hot_plug: Switch = None
+    vxlan_gpe_port: int | None = None
+    geneve_parsed_port: int | None = None
+    lock_all_memory: YesNoSwitch = field(default=None, metadata=Params.long("mlockall"))
+    mempool_allocation_mode: SimpleMempoolAllocationMode | AnonMempoolAllocationMode | None = field(
+        default=None, metadata=Params.long("mp-alloc")
+    )
+    record_core_cycles: Switch = None
+    record_burst_status: Switch = None
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index e71cda406f..9deeb2a3a5 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -26,7 +26,7 @@
 from typing_extensions import Self
 
 from framework.exception import InteractiveCommandExecutionError
-from framework.params.eal import EalParams
+from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
 from framework.parser import ParserFn, TextParser
 from framework.settings import SETTINGS
 from framework.utils import StrEnum
@@ -56,37 +56,6 @@ def __str__(self) -> str:
         return self.pci_address
 
 
-class TestPmdForwardingModes(StrEnum):
-    r"""The supported packet forwarding modes for :class:`~TestPmdShell`\s."""
-
-    #:
-    io = auto()
-    #:
-    mac = auto()
-    #:
-    macswap = auto()
-    #:
-    flowgen = auto()
-    #:
-    rxonly = auto()
-    #:
-    txonly = auto()
-    #:
-    csum = auto()
-    #:
-    icmpecho = auto()
-    #:
-    ieee1588 = auto()
-    #:
-    noisy = auto()
-    #:
-    fivetswap = "5tswap"
-    #:
-    shared_rxq = "shared-rxq"
-    #:
-    recycle_mbufs = auto()
-
-
 class VLANOffloadFlag(Flag):
     """Flag representing the VLAN offload settings of a NIC port."""
 
@@ -646,9 +615,7 @@ def _start_application(self, get_privileged_command: Callable[[str], str] | None
         Also find the number of pci addresses which were allowed on the command line when the app
         was started.
         """
-        self._app_params += " -i --mask-event intr_lsc"
-
-        assert isinstance(self._app_params, EalParams)
+        assert isinstance(self._app_params, TestPmdParams)
 
         self.number_of_ports = (
             len(self._app_params.ports) if self._app_params.ports is not None else 0
@@ -740,7 +707,7 @@ def wait_link_status_up(self, port_id: int, timeout=SETTINGS.timeout) -> bool:
             self._logger.error(f"The link for port {port_id} did not come up in the given timeout.")
         return "Link status: up" in port_info
 
-    def set_forward_mode(self, mode: TestPmdForwardingModes, verify: bool = True):
+    def set_forward_mode(self, mode: SimpleForwardingModes, verify: bool = True):
         """Set packet forwarding mode.
 
         Args:
diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index 9338c35651..a50a72d6f5 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -23,7 +23,8 @@
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
 from framework.params import Params
-from framework.remote_session.testpmd_shell import TestPmdForwardingModes, TestPmdShell
+from framework.params.testpmd import SimpleForwardingModes
+from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.test_suite import TestSuite
 
 
@@ -113,7 +114,7 @@ def pmd_scatter(self, mbsize: int) -> None:
             ),
             privileged=True,
         )
-        testpmd.set_forward_mode(TestPmdForwardingModes.mac)
+        testpmd.set_forward_mode(SimpleForwardingModes.mac)
         testpmd.start()
 
         for offset in [-1, 0, 1, 4, 5]:
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v7 6/8] dts: use testpmd params for scatter test suite
  2024-06-19 14:02 ` [PATCH v7 0/8] dts: add testpmd params Luca Vizzarro
                     ` (4 preceding siblings ...)
  2024-06-19 14:03   ` [PATCH v7 5/8] dts: add testpmd shell params Luca Vizzarro
@ 2024-06-19 14:03   ` Luca Vizzarro
  2024-06-19 14:03   ` [PATCH v7 7/8] dts: rework interactive shells Luca Vizzarro
                     ` (2 subsequent siblings)
  8 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-19 14:03 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Update the buffer scatter test suite to use TestPmdParameters
instead of the StrParams implementation.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 dts/tests/TestSuite_pmd_buffer_scatter.py | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index a50a72d6f5..92f2e257c0 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -16,14 +16,14 @@
 """
 
 import struct
+from dataclasses import asdict
 
 from scapy.layers.inet import IP  # type: ignore[import-untyped]
 from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
 from scapy.packet import Raw  # type: ignore[import-untyped]
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
-from framework.params import Params
-from framework.params.testpmd import SimpleForwardingModes
+from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
 from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.test_suite import TestSuite
 
@@ -105,16 +105,16 @@ def pmd_scatter(self, mbsize: int) -> None:
         """
         testpmd = self.sut_node.create_interactive_shell(
             TestPmdShell,
-            app_params=Params.from_str(
-                "--mbcache=200 "
-                f"--mbuf-size={mbsize} "
-                "--max-pkt-len=9000 "
-                "--port-topology=paired "
-                "--tx-offloads=0x00008000"
+            app_params=TestPmdParams(
+                forward_mode=SimpleForwardingModes.mac,
+                mbcache=200,
+                mbuf_size=[mbsize],
+                max_pkt_len=9000,
+                tx_offloads=0x00008000,
+                **asdict(self.sut_node.create_eal_parameters()),
             ),
             privileged=True,
         )
-        testpmd.set_forward_mode(SimpleForwardingModes.mac)
         testpmd.start()
 
         for offset in [-1, 0, 1, 4, 5]:
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v7 7/8] dts: rework interactive shells
  2024-06-19 14:02 ` [PATCH v7 0/8] dts: add testpmd params Luca Vizzarro
                     ` (5 preceding siblings ...)
  2024-06-19 14:03   ` [PATCH v7 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
@ 2024-06-19 14:03   ` Luca Vizzarro
  2024-06-19 14:03   ` [PATCH v7 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
  2024-06-20  3:36   ` [PATCH v7 0/8] dts: add testpmd params Thomas Monjalon
  8 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-19 14:03 UTC (permalink / raw)
  To: dev; +Cc: Jeremy Spewock, Juraj Linkeš, Luca Vizzarro, Paul Szczepanek

The way nodes and interactive shells interact makes it difficult to
develop for static type checking and hinting. The current system relies
on a top-down approach, attempting to give a generic interface to the
test developer, hiding the interaction of concrete shell classes as much
as possible. When working with strong typing this approach is not ideal,
as Python's implementation of generics is still rudimentary.

This rework reverses the tests interaction to a bottom-up approach,
allowing the test developer to call concrete shell classes directly,
and let them ingest nodes independently. While also re-enforcing type
checking and making the code easier to read.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
---
 dts/framework/params/eal.py                   |   6 +-
 dts/framework/remote_session/dpdk_shell.py    | 105 +++++++++++++++
 .../remote_session/interactive_shell.py       |  75 ++++++-----
 dts/framework/remote_session/python_shell.py  |   4 +-
 dts/framework/remote_session/testpmd_shell.py |  64 +++++----
 dts/framework/testbed_model/node.py           |  36 +----
 dts/framework/testbed_model/os_session.py     |  36 +----
 dts/framework/testbed_model/sut_node.py       | 123 +-----------------
 .../testbed_model/traffic_generator/scapy.py  |   4 +-
 dts/tests/TestSuite_hello_world.py            |   7 +-
 dts/tests/TestSuite_pmd_buffer_scatter.py     |  21 ++-
 dts/tests/TestSuite_smoke_tests.py            |   2 +-
 12 files changed, 205 insertions(+), 278 deletions(-)
 create mode 100644 dts/framework/remote_session/dpdk_shell.py

diff --git a/dts/framework/params/eal.py b/dts/framework/params/eal.py
index bbdbc8f334..8d7766fefc 100644
--- a/dts/framework/params/eal.py
+++ b/dts/framework/params/eal.py
@@ -35,9 +35,9 @@ class EalParams(Params):
                 ``other_eal_param='--single-file-segments'``
     """
 
-    lcore_list: LogicalCoreList = field(metadata=Params.short("l"))
-    memory_channels: int = field(metadata=Params.short("n"))
-    prefix: str = field(metadata=Params.long("file-prefix"))
+    lcore_list: LogicalCoreList | None = field(default=None, metadata=Params.short("l"))
+    memory_channels: int | None = field(default=None, metadata=Params.short("n"))
+    prefix: str = field(default="dpdk", metadata=Params.long("file-prefix"))
     no_pci: Switch = None
     vdevs: list[VirtualDevice] | None = field(
         default=None, metadata=Params.multiple() | Params.long("vdev")
diff --git a/dts/framework/remote_session/dpdk_shell.py b/dts/framework/remote_session/dpdk_shell.py
new file mode 100644
index 0000000000..296639f37d
--- /dev/null
+++ b/dts/framework/remote_session/dpdk_shell.py
@@ -0,0 +1,105 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Base interactive shell for DPDK applications.
+
+Provides a base class to create interactive shells based on DPDK.
+"""
+
+
+from abc import ABC
+from pathlib import PurePath
+
+from framework.params.eal import EalParams
+from framework.remote_session.interactive_shell import InteractiveShell
+from framework.settings import SETTINGS
+from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
+from framework.testbed_model.sut_node import SutNode
+
+
+def compute_eal_params(
+    sut_node: SutNode,
+    params: EalParams | None = None,
+    lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
+    ascending_cores: bool = True,
+    append_prefix_timestamp: bool = True,
+) -> EalParams:
+    """Compute EAL parameters based on the node's specifications.
+
+    Args:
+        sut_node: The SUT node to compute the values for.
+        params: If :data:`None`, a new object is created and returned. Otherwise `params.lcore_list`
+            is modified according to `lcore_filter_specifier`. A DPDK file prefix is also added. If
+            `params.ports` is :data:`None`, then `sut_node.ports` is assigned to it.
+        lcore_filter_specifier: A number of lcores/cores/sockets to use or a list of lcore ids to
+            use. The default will select one lcore for each of two cores on one socket, in ascending
+            order of core ids.
+        ascending_cores: Sort cores in ascending order (lowest to highest IDs). If :data:`False`,
+            sort in descending order.
+        append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.
+    """
+    if params is None:
+        params = EalParams()
+
+    if params.lcore_list is None:
+        params.lcore_list = LogicalCoreList(
+            sut_node.filter_lcores(lcore_filter_specifier, ascending_cores)
+        )
+
+    prefix = params.prefix
+    if append_prefix_timestamp:
+        prefix = f"{prefix}_{sut_node.dpdk_timestamp}"
+    prefix = sut_node.main_session.get_dpdk_file_prefix(prefix)
+    if prefix:
+        sut_node.dpdk_prefix_list.append(prefix)
+    params.prefix = prefix
+
+    if params.ports is None:
+        params.ports = sut_node.ports
+
+    return params
+
+
+class DPDKShell(InteractiveShell, ABC):
+    """The base class for managing DPDK-based interactive shells.
+
+    This class shouldn't be instantiated directly, but instead be extended.
+    It automatically injects computed EAL parameters based on the node in the
+    supplied app parameters.
+    """
+
+    _node: SutNode
+    _app_params: EalParams
+
+    def __init__(
+        self,
+        node: SutNode,
+        privileged: bool = True,
+        timeout: float = SETTINGS.timeout,
+        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
+        ascending_cores: bool = True,
+        append_prefix_timestamp: bool = True,
+        start_on_init: bool = True,
+        app_params: EalParams = EalParams(),
+    ) -> None:
+        """Extends :meth:`~.interactive_shell.InteractiveShell.__init__`.
+
+        Adds the `lcore_filter_specifier`, `ascending_cores` and `append_prefix_timestamp` arguments
+        which are then used to compute the EAL parameters based on the node's configuration.
+        """
+        app_params = compute_eal_params(
+            node,
+            app_params,
+            lcore_filter_specifier,
+            ascending_cores,
+            append_prefix_timestamp,
+        )
+
+        super().__init__(node, privileged, timeout, start_on_init, app_params)
+
+    def _update_real_path(self, path: PurePath) -> None:
+        """Extends :meth:`~.interactive_shell.InteractiveShell._update_real_path`.
+
+        Adds the remote DPDK build directory to the path.
+        """
+        super()._update_real_path(self._node.remote_dpdk_build_dir.joinpath(path))
diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
index 8191b36630..254aa29f89 100644
--- a/dts/framework/remote_session/interactive_shell.py
+++ b/dts/framework/remote_session/interactive_shell.py
@@ -17,13 +17,14 @@
 
 from abc import ABC
 from pathlib import PurePath
-from typing import Callable, ClassVar
+from typing import ClassVar
 
-from paramiko import Channel, SSHClient, channel  # type: ignore[import-untyped]
+from paramiko import Channel, channel  # type: ignore[import-untyped]
 
 from framework.logger import DTSLogger
 from framework.params import Params
 from framework.settings import SETTINGS
+from framework.testbed_model.node import Node
 
 
 class InteractiveShell(ABC):
@@ -36,13 +37,15 @@ class InteractiveShell(ABC):
     session.
     """
 
-    _interactive_session: SSHClient
+    _node: Node
     _stdin: channel.ChannelStdinFile
     _stdout: channel.ChannelFile
     _ssh_channel: Channel
     _logger: DTSLogger
     _timeout: float
     _app_params: Params
+    _privileged: bool
+    _real_path: PurePath
 
     #: Prompt to expect at the end of output when sending a command.
     #: This is often overridden by subclasses.
@@ -56,56 +59,58 @@ class InteractiveShell(ABC):
     #: Path to the executable to start the interactive application.
     path: ClassVar[PurePath]
 
-    #: Whether this application is a DPDK app. If it is, the build directory
-    #: for DPDK on the node will be prepended to the path to the executable.
-    dpdk_app: ClassVar[bool] = False
-
     def __init__(
         self,
-        interactive_session: SSHClient,
-        logger: DTSLogger,
-        get_privileged_command: Callable[[str], str] | None,
-        app_params: Params = Params(),
+        node: Node,
+        privileged: bool = False,
         timeout: float = SETTINGS.timeout,
+        start_on_init: bool = True,
+        app_params: Params = Params(),
     ) -> None:
         """Create an SSH channel during initialization.
 
         Args:
-            interactive_session: The SSH session dedicated to interactive shells.
-            logger: The logger instance this session will use.
-            get_privileged_command: A method for modifying a command to allow it to use
-                elevated privileges. If :data:`None`, the application will not be started
-                with elevated privileges.
-            app_params: The command line parameters to be passed to the application on startup.
+            node: The node on which to run start the interactive shell.
+            privileged: Enables the shell to run as superuser.
             timeout: The timeout used for the SSH channel that is dedicated to this interactive
                 shell. This timeout is for collecting output, so if reading from the buffer
                 and no output is gathered within the timeout, an exception is thrown.
+            start_on_init: Start interactive shell automatically after object initialisation.
+            app_params: The command line parameters to be passed to the application on startup.
         """
-        self._interactive_session = interactive_session
-        self._ssh_channel = self._interactive_session.invoke_shell()
+        self._node = node
+        self._logger = node._logger
+        self._app_params = app_params
+        self._privileged = privileged
+        self._timeout = timeout
+        # Ensure path is properly formatted for the host
+        self._update_real_path(self.path)
+
+        if start_on_init:
+            self.start_application()
+
+    def _setup_ssh_channel(self):
+        self._ssh_channel = self._node.main_session.interactive_session.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.settimeout(self._timeout)
         self._ssh_channel.set_combine_stderr(True)  # combines stdout and stderr streams
-        self._logger = logger
-        self._timeout = timeout
-        self._app_params = app_params
-        self._start_application(get_privileged_command)
 
-    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
+    def _make_start_command(self) -> str:
+        """Makes the command that starts the interactive shell."""
+        start_command = f"{self._real_path} {self._app_params or ''}"
+        if self._privileged:
+            start_command = self._node.main_session._get_privileged_command(start_command)
+        return start_command
+
+    def start_application(self) -> None:
         """Starts a new interactive application based on the path to the app.
 
         This method is often overridden by subclasses as their process for
         starting may look different.
-
-        Args:
-            get_privileged_command: A function (but could be any callable) that produces
-                the version of the command with elevated privileges.
         """
-        start_command = f"{self.path} {self._app_params}"
-        if get_privileged_command is not None:
-            start_command = get_privileged_command(start_command)
-        self.send_command(start_command)
+        self._setup_ssh_channel()
+        self.send_command(self._make_start_command())
 
     def send_command(
         self, command: str, prompt: str | None = None, skip_first_line: bool = False
@@ -156,3 +161,7 @@ def close(self) -> None:
     def __del__(self) -> None:
         """Make sure the session is properly closed before deleting the object."""
         self.close()
+
+    def _update_real_path(self, path: PurePath) -> None:
+        """Updates the interactive shell's real path used at command line."""
+        self._real_path = self._node.main_session.join_remote_path(path)
diff --git a/dts/framework/remote_session/python_shell.py b/dts/framework/remote_session/python_shell.py
index ccfd3783e8..953ed100df 100644
--- a/dts/framework/remote_session/python_shell.py
+++ b/dts/framework/remote_session/python_shell.py
@@ -6,9 +6,7 @@
 Typical usage example in a TestSuite::
 
     from framework.remote_session import PythonShell
-    python_shell = self.tg_node.create_interactive_shell(
-        PythonShell, timeout=5, privileged=True
-    )
+    python_shell = PythonShell(self.tg_node, timeout=5, privileged=True)
     python_shell.send_command("print('Hello World')")
     python_shell.close()
 """
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index 9deeb2a3a5..a8882a32fd 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -7,9 +7,7 @@
 
 Typical usage example in a TestSuite::
 
-    testpmd_shell = self.sut_node.create_interactive_shell(
-            TestPmdShell, privileged=True
-        )
+    testpmd_shell = TestPmdShell(self.sut_node)
     devices = testpmd_shell.get_devices()
     for device in devices:
         print(device)
@@ -21,18 +19,19 @@
 from dataclasses import dataclass, field
 from enum import Flag, auto
 from pathlib import PurePath
-from typing import Callable, ClassVar
+from typing import ClassVar
 
 from typing_extensions import Self
 
 from framework.exception import InteractiveCommandExecutionError
 from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
 from framework.parser import ParserFn, TextParser
+from framework.remote_session.dpdk_shell import DPDKShell
 from framework.settings import SETTINGS
+from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
+from framework.testbed_model.sut_node import SutNode
 from framework.utils import StrEnum
 
-from .interactive_shell import InteractiveShell
-
 
 class TestPmdDevice:
     """The data of a device that testpmd can recognize.
@@ -577,52 +576,48 @@ class TestPmdPortStats(TextParser):
     tx_bps: int = field(metadata=TextParser.find_int(r"Tx-bps:\s+(\d+)"))
 
 
-class TestPmdShell(InteractiveShell):
+class TestPmdShell(DPDKShell):
     """Testpmd interactive shell.
 
     The testpmd shell users should never use
     the :meth:`~.interactive_shell.InteractiveShell.send_command` method directly, but rather
     call specialized methods. If there isn't one that satisfies a need, it should be added.
-
-    Attributes:
-        number_of_ports: The number of ports which were allowed on the command-line when testpmd
-            was started.
     """
 
-    number_of_ports: int
+    _app_params: TestPmdParams
 
     #: The path to the testpmd executable.
     path: ClassVar[PurePath] = PurePath("app", "dpdk-testpmd")
 
-    #: Flag this as a DPDK app so that it's clear this is not a system app and
-    #: needs to be looked in a specific path.
-    dpdk_app: ClassVar[bool] = True
-
     #: The testpmd's prompt.
     _default_prompt: ClassVar[str] = "testpmd>"
 
     #: This forces the prompt to appear after sending a command.
     _command_extra_chars: ClassVar[str] = "\n"
 
-    def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
-        """Overrides :meth:`~.interactive_shell._start_application`.
-
-        Add flags for starting testpmd in interactive mode and disabling messages for link state
-        change events before starting the application. Link state is verified before starting
-        packet forwarding and the messages create unexpected newlines in the terminal which
-        complicates output collection.
-
-        Also find the number of pci addresses which were allowed on the command line when the app
-        was started.
-        """
-        assert isinstance(self._app_params, TestPmdParams)
-
-        self.number_of_ports = (
-            len(self._app_params.ports) if self._app_params.ports is not None else 0
+    def __init__(
+        self,
+        node: SutNode,
+        privileged: bool = True,
+        timeout: float = SETTINGS.timeout,
+        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
+        ascending_cores: bool = True,
+        append_prefix_timestamp: bool = True,
+        start_on_init: bool = True,
+        **app_params,
+    ) -> None:
+        """Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs."""
+        super().__init__(
+            node,
+            privileged,
+            timeout,
+            lcore_filter_specifier,
+            ascending_cores,
+            append_prefix_timestamp,
+            start_on_init,
+            TestPmdParams(**app_params),
         )
 
-        super()._start_application(get_privileged_command)
-
     def start(self, verify: bool = True) -> None:
         """Start packet forwarding with the current configuration.
 
@@ -642,7 +637,8 @@ def start(self, verify: bool = True) -> None:
                 self._logger.debug(f"Failed to start packet forwarding: \n{start_cmd_output}")
                 raise InteractiveCommandExecutionError("Testpmd failed to start packet forwarding.")
 
-            for port_id in range(self.number_of_ports):
+            number_of_ports = len(self._app_params.ports or [])
+            for port_id in range(number_of_ports):
                 if not self.wait_link_status_up(port_id):
                     raise InteractiveCommandExecutionError(
                         "Not all ports came up after starting packet forwarding in testpmd."
diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
index 4ad2924c93..12a40170ac 100644
--- a/dts/framework/testbed_model/node.py
+++ b/dts/framework/testbed_model/node.py
@@ -15,12 +15,11 @@
 
 from abc import ABC
 from ipaddress import IPv4Interface, IPv6Interface
-from typing import Any, Callable, Type, Union
+from typing import Any, Callable, Union
 
 from framework.config import OS, NodeConfiguration, TestRunConfiguration
 from framework.exception import ConfigurationError
 from framework.logger import DTSLogger, get_dts_logger
-from framework.params import Params
 from framework.settings import SETTINGS
 
 from .cpu import (
@@ -31,7 +30,7 @@
     lcore_filter,
 )
 from .linux_session import LinuxSession
-from .os_session import InteractiveShellType, OSSession
+from .os_session import OSSession
 from .port import Port
 
 
@@ -140,37 +139,6 @@ def create_session(self, name: str) -> OSSession:
         self._other_sessions.append(connection)
         return connection
 
-    def create_interactive_shell(
-        self,
-        shell_cls: Type[InteractiveShellType],
-        timeout: float = SETTINGS.timeout,
-        privileged: bool = False,
-        app_params: Params = Params(),
-    ) -> InteractiveShellType:
-        """Factory for interactive session handlers.
-
-        Instantiate `shell_cls` according to the remote OS specifics.
-
-        Args:
-            shell_cls: The class of the shell.
-            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.
-            privileged: Whether to run the shell with administrative privileges.
-            app_args: The arguments to be passed to the application.
-
-        Returns:
-            An instance of the desired interactive application shell.
-        """
-        if not shell_cls.dpdk_app:
-            shell_cls.path = self.main_session.join_remote_path(shell_cls.path)
-
-        return self.main_session.create_interactive_shell(
-            shell_cls,
-            timeout,
-            privileged,
-            app_params,
-        )
-
     def filter_lcores(
         self,
         filter_specifier: LogicalCoreCount | LogicalCoreList,
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
index 84000931ff..79f56b289b 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -26,18 +26,16 @@
 from collections.abc import Iterable
 from ipaddress import IPv4Interface, IPv6Interface
 from pathlib import PurePath
-from typing import Type, TypeVar, Union
+from typing import Union
 
 from framework.config import Architecture, NodeConfiguration, NodeInfo
 from framework.logger import DTSLogger
-from framework.params import Params
 from framework.remote_session import (
     InteractiveRemoteSession,
     RemoteSession,
     create_interactive_session,
     create_remote_session,
 )
-from framework.remote_session.interactive_shell import InteractiveShell
 from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
@@ -45,8 +43,6 @@
 from .cpu import LogicalCore
 from .port import Port
 
-InteractiveShellType = TypeVar("InteractiveShellType", bound=InteractiveShell)
-
 
 class OSSession(ABC):
     """OS-unaware to OS-aware translation API definition.
@@ -125,36 +121,6 @@ def send_command(
 
         return self.remote_session.send_command(command, timeout, verify, env)
 
-    def create_interactive_shell(
-        self,
-        shell_cls: Type[InteractiveShellType],
-        timeout: float,
-        privileged: bool,
-        app_args: Params,
-    ) -> InteractiveShellType:
-        """Factory for interactive session handlers.
-
-        Instantiate `shell_cls` according to the remote OS specifics.
-
-        Args:
-            shell_cls: The class of the shell.
-            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.
-            privileged: Whether to run the shell with administrative privileges.
-            app_args: The arguments to be passed to the application.
-
-        Returns:
-            An instance of the desired interactive application shell.
-        """
-        return shell_cls(
-            self.interactive_session.session,
-            self._logger,
-            self._get_privileged_command if privileged else None,
-            app_args,
-            timeout,
-        )
-
     def close(self) -> None:
         """Close the underlying remote session."""
         self.remote_session.close()
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py
index 9bb4fc78b1..2855fe0276 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -16,7 +16,6 @@
 import tarfile
 import time
 from pathlib import PurePath
-from typing import Type
 
 from framework.config import (
     BuildTargetConfiguration,
@@ -25,16 +24,13 @@
     SutNodeConfiguration,
     TestRunConfiguration,
 )
-from framework.params import Params, Switch
 from framework.params.eal import EalParams
 from framework.remote_session.remote_session import CommandResult
 from framework.settings import SETTINGS
 from framework.utils import MesonArgs
 
-from .cpu import LogicalCoreCount, LogicalCoreList
 from .node import Node
-from .os_session import InteractiveShellType, OSSession
-from .port import Port
+from .os_session import OSSession
 from .virtual_device import VirtualDevice
 
 
@@ -59,8 +55,8 @@ class SutNode(Node):
 
     config: SutNodeConfiguration
     virtual_devices: list[VirtualDevice]
-    _dpdk_prefix_list: list[str]
-    _dpdk_timestamp: str
+    dpdk_prefix_list: list[str]
+    dpdk_timestamp: str
     _build_target_config: BuildTargetConfiguration | None
     _env_vars: dict
     _remote_tmp_dir: PurePath
@@ -80,14 +76,14 @@ def __init__(self, node_config: SutNodeConfiguration):
         """
         super().__init__(node_config)
         self.virtual_devices = []
-        self._dpdk_prefix_list = []
+        self.dpdk_prefix_list = []
         self._build_target_config = None
         self._env_vars = {}
         self._remote_tmp_dir = self.main_session.get_remote_tmp_dir()
         self.__remote_dpdk_dir = None
         self._app_compile_timeout = 90
         self._dpdk_kill_session = None
-        self._dpdk_timestamp = (
+        self.dpdk_timestamp = (
             f"{str(os.getpid())}_{time.strftime('%Y%m%d%H%M%S', time.localtime())}"
         )
         self._dpdk_version = None
@@ -306,73 +302,11 @@ def kill_cleanup_dpdk_apps(self) -> None:
         """Kill all dpdk applications on the SUT, then clean up hugepages."""
         if self._dpdk_kill_session and self._dpdk_kill_session.is_alive():
             # we can use the session if it exists and responds
-            self._dpdk_kill_session.kill_cleanup_dpdk_apps(self._dpdk_prefix_list)
+            self._dpdk_kill_session.kill_cleanup_dpdk_apps(self.dpdk_prefix_list)
         else:
             # otherwise, we need to (re)create it
             self._dpdk_kill_session = self.create_session("dpdk_kill")
-        self._dpdk_prefix_list = []
-
-    def create_eal_parameters(
-        self,
-        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
-        ascending_cores: bool = True,
-        prefix: str = "dpdk",
-        append_prefix_timestamp: bool = True,
-        no_pci: Switch = None,
-        vdevs: list[VirtualDevice] | None = None,
-        ports: list[Port] | None = None,
-        other_eal_param: str = "",
-    ) -> EalParams:
-        """Compose the EAL parameters.
-
-        Process the list of cores and the DPDK prefix and pass that along with
-        the rest of the arguments.
-
-        Args:
-            lcore_filter_specifier: A number of lcores/cores/sockets to use
-                or a list of lcore ids to use.
-                The default will select one lcore for each of two cores
-                on one socket, in ascending order of core ids.
-            ascending_cores: Sort cores in ascending order (lowest to highest IDs).
-                If :data:`False`, sort in descending order.
-            prefix: Set the file prefix string with which to start DPDK, e.g.: ``prefix='vf'``.
-            append_prefix_timestamp: If :data:`True`, will append a timestamp to DPDK file prefix.
-            no_pci: Switch to disable PCI bus e.g.: ``no_pci=True``.
-            vdevs: Virtual devices, e.g.::
-
-                vdevs=[
-                    VirtualDevice('net_ring0'),
-                    VirtualDevice('net_ring1')
-                ]
-            ports: The list of ports to allow. If :data:`None`, all ports listed in `self.ports`
-                will be allowed.
-            other_eal_param: user defined DPDK EAL parameters, e.g.:
-                ``other_eal_param='--single-file-segments'``.
-
-        Returns:
-            An EAL param string, such as
-            ``-c 0xf -a 0000:88:00.0 --file-prefix=dpdk_1112_20190809143420``.
-        """
-        lcore_list = LogicalCoreList(self.filter_lcores(lcore_filter_specifier, ascending_cores))
-
-        if append_prefix_timestamp:
-            prefix = f"{prefix}_{self._dpdk_timestamp}"
-        prefix = self.main_session.get_dpdk_file_prefix(prefix)
-        if prefix:
-            self._dpdk_prefix_list.append(prefix)
-
-        if ports is None:
-            ports = self.ports
-
-        return EalParams(
-            lcore_list=lcore_list,
-            memory_channels=self.config.memory_channels,
-            prefix=prefix,
-            no_pci=no_pci,
-            vdevs=vdevs,
-            ports=ports,
-            other_eal_param=Params.from_str(other_eal_param),
-        )
+        self.dpdk_prefix_list = []
 
     def run_dpdk_app(
         self, app_path: PurePath, eal_params: EalParams, timeout: float = 30
@@ -402,49 +336,6 @@ def configure_ipv4_forwarding(self, enable: bool) -> None:
         """
         self.main_session.configure_ipv4_forwarding(enable)
 
-    def create_interactive_shell(
-        self,
-        shell_cls: Type[InteractiveShellType],
-        timeout: float = SETTINGS.timeout,
-        privileged: bool = False,
-        app_params: Params = Params(),
-        eal_params: EalParams | None = None,
-    ) -> InteractiveShellType:
-        """Extend the factory for interactive session handlers.
-
-        The extensions are SUT node specific:
-
-            * The default for `eal_parameters`,
-            * The interactive shell path `shell_cls.path` is prepended with path to the remote
-              DPDK build directory for DPDK apps.
-
-        Args:
-            shell_cls: The class of the shell.
-            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.
-            privileged: Whether to run the shell with administrative privileges.
-            app_params: The parameters to be passed to the application.
-            eal_params: List of EAL parameters to use to launch the app. If this
-                isn't provided or an empty string is passed, it will default to calling
-                :meth:`create_eal_parameters`.
-
-        Returns:
-            An instance of the desired interactive application shell.
-        """
-        # We need to append the build directory and add EAL parameters for DPDK apps
-        if shell_cls.dpdk_app:
-            if eal_params is None:
-                eal_params = self.create_eal_parameters()
-            eal_params.append_str(str(app_params))
-            app_params = eal_params
-
-            shell_cls.path = self.main_session.join_remote_path(
-                self.remote_dpdk_build_dir, shell_cls.path
-            )
-
-        return super().create_interactive_shell(shell_cls, timeout, privileged, app_params)
-
     def bind_ports_to_driver(self, for_dpdk: bool = True) -> None:
         """Bind all ports on the SUT to a driver.
 
diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py
index 7bc1c2cc08..bf58ad1c5e 100644
--- a/dts/framework/testbed_model/traffic_generator/scapy.py
+++ b/dts/framework/testbed_model/traffic_generator/scapy.py
@@ -217,9 +217,7 @@ def __init__(self, tg_node: Node, config: ScapyTrafficGeneratorConfig):
             self._tg_node.config.os == OS.linux
         ), "Linux is the only supported OS for scapy traffic generation"
 
-        self.session = self._tg_node.create_interactive_shell(
-            PythonShell, timeout=5, privileged=True
-        )
+        self.session = PythonShell(self._tg_node, timeout=5, privileged=True)
 
         # import libs in remote python console
         for import_statement in SCAPY_RPC_SERVER_IMPORTS:
diff --git a/dts/tests/TestSuite_hello_world.py b/dts/tests/TestSuite_hello_world.py
index 0d6995f260..d958f99030 100644
--- a/dts/tests/TestSuite_hello_world.py
+++ b/dts/tests/TestSuite_hello_world.py
@@ -7,6 +7,7 @@
 No other EAL parameters apart from cores are used.
 """
 
+from framework.remote_session.dpdk_shell import compute_eal_params
 from framework.test_suite import TestSuite
 from framework.testbed_model.cpu import (
     LogicalCoreCount,
@@ -38,7 +39,7 @@ def test_hello_world_single_core(self) -> None:
         # get the first usable core
         lcore_amount = LogicalCoreCount(1, 1, 1)
         lcores = LogicalCoreCountFilter(self.sut_node.lcores, lcore_amount).filter()
-        eal_para = self.sut_node.create_eal_parameters(lcore_filter_specifier=lcore_amount)
+        eal_para = compute_eal_params(self.sut_node, lcore_filter_specifier=lcore_amount)
         result = self.sut_node.run_dpdk_app(self.app_helloworld_path, eal_para)
         self.verify(
             f"hello from core {int(lcores[0])}" in result.stdout,
@@ -55,8 +56,8 @@ def test_hello_world_all_cores(self) -> None:
             "hello from core <core_id>"
         """
         # get the maximum logical core number
-        eal_para = self.sut_node.create_eal_parameters(
-            lcore_filter_specifier=LogicalCoreList(self.sut_node.lcores)
+        eal_para = compute_eal_params(
+            self.sut_node, lcore_filter_specifier=LogicalCoreList(self.sut_node.lcores)
         )
         result = self.sut_node.run_dpdk_app(self.app_helloworld_path, eal_para, 50)
         for lcore in self.sut_node.lcores:
diff --git a/dts/tests/TestSuite_pmd_buffer_scatter.py b/dts/tests/TestSuite_pmd_buffer_scatter.py
index 92f2e257c0..d954545330 100644
--- a/dts/tests/TestSuite_pmd_buffer_scatter.py
+++ b/dts/tests/TestSuite_pmd_buffer_scatter.py
@@ -16,14 +16,13 @@
 """
 
 import struct
-from dataclasses import asdict
 
 from scapy.layers.inet import IP  # type: ignore[import-untyped]
 from scapy.layers.l2 import Ether  # type: ignore[import-untyped]
 from scapy.packet import Raw  # type: ignore[import-untyped]
 from scapy.utils import hexstr  # type: ignore[import-untyped]
 
-from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
+from framework.params.testpmd import SimpleForwardingModes
 from framework.remote_session.testpmd_shell import TestPmdShell
 from framework.test_suite import TestSuite
 
@@ -103,17 +102,13 @@ def pmd_scatter(self, mbsize: int) -> None:
         Test:
             Start testpmd and run functional test with preset mbsize.
         """
-        testpmd = self.sut_node.create_interactive_shell(
-            TestPmdShell,
-            app_params=TestPmdParams(
-                forward_mode=SimpleForwardingModes.mac,
-                mbcache=200,
-                mbuf_size=[mbsize],
-                max_pkt_len=9000,
-                tx_offloads=0x00008000,
-                **asdict(self.sut_node.create_eal_parameters()),
-            ),
-            privileged=True,
+        testpmd = TestPmdShell(
+            self.sut_node,
+            forward_mode=SimpleForwardingModes.mac,
+            mbcache=200,
+            mbuf_size=[mbsize],
+            max_pkt_len=9000,
+            tx_offloads=0x00008000,
         )
         testpmd.start()
 
diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py
index ca678f662d..eca27acfd8 100644
--- a/dts/tests/TestSuite_smoke_tests.py
+++ b/dts/tests/TestSuite_smoke_tests.py
@@ -99,7 +99,7 @@ def test_devices_listed_in_testpmd(self) -> None:
         Test:
             List all devices found in testpmd and verify the configured devices are among them.
         """
-        testpmd_driver = self.sut_node.create_interactive_shell(TestPmdShell, privileged=True)
+        testpmd_driver = TestPmdShell(self.sut_node)
         dev_list = [str(x) for x in testpmd_driver.get_devices()]
         for nic in self.nics_in_node:
             self.verify(
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* [PATCH v7 8/8] dts: use Unpack for type checking and hinting
  2024-06-19 14:02 ` [PATCH v7 0/8] dts: add testpmd params Luca Vizzarro
                     ` (6 preceding siblings ...)
  2024-06-19 14:03   ` [PATCH v7 7/8] dts: rework interactive shells Luca Vizzarro
@ 2024-06-19 14:03   ` Luca Vizzarro
  2024-06-20  3:36   ` [PATCH v7 0/8] dts: add testpmd params Thomas Monjalon
  8 siblings, 0 replies; 159+ messages in thread
From: Luca Vizzarro @ 2024-06-19 14:03 UTC (permalink / raw)
  To: dev
  Cc: Jeremy Spewock, Juraj Linkeš,
	Luca Vizzarro, Paul Szczepanek, Nicholas Pratte

Interactive shells that inherit DPDKShell initialise their params
classes from a kwargs dict. Therefore, static type checking is
disabled. This change uses the functionality of Unpack added in
PEP 692 to re-enable it. The disadvantage is that this functionality has
been implemented only with TypedDict, forcing the creation of TypedDict
mirrors of the Params classes.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Reviewed-by: Juraj Linkeš <juraj.linkes@pantheon.tech>
Reviewed-by: Jeremy Spewock <jspewock@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npratte@iol.unh.edu>
---
 dts/framework/params/types.py                 | 133 ++++++++++++++++++
 dts/framework/remote_session/testpmd_shell.py |   5 +-
 2 files changed, 136 insertions(+), 2 deletions(-)
 create mode 100644 dts/framework/params/types.py

diff --git a/dts/framework/params/types.py b/dts/framework/params/types.py
new file mode 100644
index 0000000000..e668f658d8
--- /dev/null
+++ b/dts/framework/params/types.py
@@ -0,0 +1,133 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2024 Arm Limited
+
+"""Module containing TypeDict-equivalents of Params classes for static typing and hinting.
+
+TypedDicts can be used in conjunction with Unpack and kwargs for type hinting on function calls.
+
+Example:
+    ..code:: python
+        def create_testpmd(**kwargs: Unpack[TestPmdParamsDict]):
+            params = TestPmdParams(**kwargs)
+"""
+
+from pathlib import PurePath
+from typing import TypedDict
+
+from framework.params import Switch, YesNoSwitch
+from framework.params.testpmd import (
+    AnonMempoolAllocationMode,
+    EthPeer,
+    Event,
+    FlowGenForwardingMode,
+    HairpinMode,
+    NoisyForwardingMode,
+    Params,
+    PortNUMAConfig,
+    PortTopology,
+    RingNUMAConfig,
+    RSSSetting,
+    RXMultiQueueMode,
+    RXRingParams,
+    SimpleForwardingModes,
+    SimpleMempoolAllocationMode,
+    TxIPAddrPair,
+    TXOnlyForwardingMode,
+    TXRingParams,
+    TxUDPPortPair,
+)
+from framework.testbed_model.cpu import LogicalCoreList
+from framework.testbed_model.port import Port
+from framework.testbed_model.virtual_device import VirtualDevice
+
+
+class EalParamsDict(TypedDict, total=False):
+    """:class:`TypedDict` equivalent of :class:`~.eal.EalParams`."""
+
+    lcore_list: LogicalCoreList | None
+    memory_channels: int | None
+    prefix: str
+    no_pci: Switch
+    vdevs: list[VirtualDevice] | None
+    ports: list[Port] | None
+    other_eal_param: Params | None
+
+
+class TestPmdParamsDict(EalParamsDict, total=False):
+    """:class:`TypedDict` equivalent of :class:`~.testpmd.TestPmdParams`."""
+
+    interactive_mode: Switch
+    auto_start: Switch
+    tx_first: Switch
+    stats_period: int | None
+    display_xstats: list[str] | None
+    nb_cores: int | None
+    coremask: int | None
+    nb_ports: int | None
+    port_topology: PortTopology | None
+    portmask: int | None
+    portlist: str | None
+    numa: YesNoSwitch
+    socket_num: int | None
+    port_numa_config: list[PortNUMAConfig] | None
+    ring_numa_config: list[RingNUMAConfig] | None
+    total_num_mbufs: int | None
+    mbuf_size: list[int] | None
+    mbcache: int | None
+    max_pkt_len: int | None
+    eth_peers_configfile: PurePath | None
+    eth_peer: list[EthPeer] | None
+    tx_ip: TxIPAddrPair | None
+    tx_udp: TxUDPPortPair | None
+    enable_lro: Switch
+    max_lro_pkt_size: int | None
+    disable_crc_strip: Switch
+    enable_scatter: Switch
+    enable_hw_vlan: Switch
+    enable_hw_vlan_filter: Switch
+    enable_hw_vlan_strip: Switch
+    enable_hw_vlan_extend: Switch
+    enable_hw_qinq_strip: Switch
+    pkt_drop_enabled: Switch
+    rss: RSSSetting | None
+    forward_mode: (
+        SimpleForwardingModes
+        | FlowGenForwardingMode
+        | TXOnlyForwardingMode
+        | NoisyForwardingMode
+        | None
+    )
+    hairpin_mode: HairpinMode | None
+    hairpin_queues: int | None
+    burst: int | None
+    enable_rx_cksum: Switch
+    rx_queues: int | None
+    rx_ring: RXRingParams | None
+    no_flush_rx: Switch
+    rx_segments_offsets: list[int] | None
+    rx_segments_length: list[int] | None
+    multi_rx_mempool: Switch
+    rx_shared_queue: Switch | int
+    rx_offloads: int | None
+    rx_mq_mode: RXMultiQueueMode | None
+    tx_queues: int | None
+    tx_ring: TXRingParams | None
+    tx_offloads: int | None
+    eth_link_speed: int | None
+    disable_link_check: Switch
+    disable_device_start: Switch
+    no_lsc_interrupt: Switch
+    no_rmv_interrupt: Switch
+    bitrate_stats: int | None
+    latencystats: int | None
+    print_events: list[Event] | None
+    mask_events: list[Event] | None
+    flow_isolate_all: Switch
+    disable_flow_flush: Switch
+    hot_plug: Switch
+    vxlan_gpe_port: int | None
+    geneve_parsed_port: int | None
+    lock_all_memory: YesNoSwitch
+    mempool_allocation_mode: SimpleMempoolAllocationMode | AnonMempoolAllocationMode | None
+    record_core_cycles: Switch
+    record_burst_status: Switch
diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py
index a8882a32fd..ec22f72221 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -21,10 +21,11 @@
 from pathlib import PurePath
 from typing import ClassVar
 
-from typing_extensions import Self
+from typing_extensions import Self, Unpack
 
 from framework.exception import InteractiveCommandExecutionError
 from framework.params.testpmd import SimpleForwardingModes, TestPmdParams
+from framework.params.types import TestPmdParamsDict
 from framework.parser import ParserFn, TextParser
 from framework.remote_session.dpdk_shell import DPDKShell
 from framework.settings import SETTINGS
@@ -604,7 +605,7 @@ def __init__(
         ascending_cores: bool = True,
         append_prefix_timestamp: bool = True,
         start_on_init: bool = True,
-        **app_params,
+        **app_params: Unpack[TestPmdParamsDict],
     ) -> None:
         """Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs."""
         super().__init__(
-- 
2.34.1


^ permalink raw reply	[flat|nested] 159+ messages in thread

* Re: [PATCH v7 0/8] dts: add testpmd params
  2024-06-19 14:02 ` [PATCH v7 0/8] dts: add testpmd params Luca Vizzarro
                     ` (7 preceding siblings ...)
  2024-06-19 14:03   ` [PATCH v7 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
@ 2024-06-20  3:36   ` Thomas Monjalon
  8 siblings, 0 replies; 159+ messages in thread
From: Thomas Monjalon @ 2024-06-20  3:36 UTC (permalink / raw)
  To: Luca Vizzarro; +Cc: dev, Jeremy Spewock, Juraj Linkeš

19/06/2024 16:02, Luca Vizzarro:
> Luca Vizzarro (8):
>   dts: add params manipulation module
>   dts: use Params for interactive shells
>   dts: refactor EalParams
>   dts: remove module-wide imports
>   dts: add testpmd shell params
>   dts: use testpmd params for scatter test suite
>   dts: rework interactive shells
>   dts: use Unpack for type checking and hinting

Applied, thanks.




^ permalink raw reply	[flat|nested] 159+ messages in thread

end of thread, other threads:[~2024-06-20  3:37 UTC | newest]

Thread overview: 159+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2024-03-26 19:04 [PATCH 0/6] dts: add testpmd params and statefulness Luca Vizzarro
2024-03-26 19:04 ` [PATCH 1/6] dts: add parameters data structure Luca Vizzarro
2024-03-28 16:48   ` Jeremy Spewock
2024-04-09 15:52     ` Luca Vizzarro
2024-04-09 12:10   ` Juraj Linkeš
2024-04-09 16:28     ` Luca Vizzarro
2024-04-10  9:15       ` Juraj Linkeš
2024-04-10  9:51         ` Luca Vizzarro
2024-04-10 10:04           ` Juraj Linkeš
2024-03-26 19:04 ` [PATCH 2/6] dts: use Params for interactive shells Luca Vizzarro
2024-03-28 16:48   ` Jeremy Spewock
2024-04-09 14:56     ` Juraj Linkeš
2024-04-10  9:34       ` Luca Vizzarro
2024-04-10  9:58         ` Juraj Linkeš
2024-05-28 15:43   ` Nicholas Pratte
2024-03-26 19:04 ` [PATCH 3/6] dts: add testpmd shell params Luca Vizzarro
2024-03-28 16:48   ` Jeremy Spewock
2024-04-09 16:37   ` Juraj Linkeš
2024-04-10 10:49     ` Luca Vizzarro
2024-04-10 13:17       ` Juraj Linkeš
2024-03-26 19:04 ` [PATCH 4/6] dts: use testpmd params for scatter test suite Luca Vizzarro
2024-04-09 19:12   ` Juraj Linkeš
2024-04-10 10:53     ` Luca Vizzarro
2024-04-10 13:18       ` Juraj Linkeš
2024-04-26 18:06         ` Jeremy Spewock
2024-04-29  7:45           ` Juraj Linkeš
2024-03-26 19:04 ` [PATCH 5/6] dts: add statefulness to InteractiveShell Luca Vizzarro
2024-03-28 16:48   ` Jeremy Spewock
2024-04-10  6:53     ` Juraj Linkeš
2024-04-10 11:27       ` Luca Vizzarro
2024-04-10 13:35         ` Juraj Linkeš
2024-04-10 14:07           ` Luca Vizzarro
2024-04-12 12:33             ` Juraj Linkeš
2024-04-29 14:48           ` Jeremy Spewock
2024-03-26 19:04 ` [PATCH 6/6] dts: add statefulness to TestPmdShell Luca Vizzarro
2024-03-28 16:48   ` Jeremy Spewock
2024-04-10  7:41     ` Juraj Linkeš
2024-04-10 11:35       ` Luca Vizzarro
2024-04-11 10:30         ` Juraj Linkeš
2024-04-11 11:47           ` Luca Vizzarro
2024-04-11 12:13             ` Juraj Linkeš
2024-04-11 13:59               ` Luca Vizzarro
2024-04-26 18:06               ` Jeremy Spewock
2024-04-29 12:06                 ` Juraj Linkeš
2024-04-10  7:50   ` Juraj Linkeš
2024-04-10 11:37     ` Luca Vizzarro
2024-05-09 11:20 ` [PATCH v2 0/8] dts: add testpmd params Luca Vizzarro
2024-05-09 11:20   ` [PATCH v2 1/8] dts: add params manipulation module Luca Vizzarro
2024-05-28 15:40     ` Nicholas Pratte
2024-05-28 21:08     ` Jeremy Spewock
2024-06-06  9:19     ` Juraj Linkeš
2024-06-17 11:44       ` Luca Vizzarro
2024-06-18  8:55         ` Juraj Linkeš
2024-05-09 11:20   ` [PATCH v2 2/8] dts: use Params for interactive shells Luca Vizzarro
2024-05-28 17:43     ` Nicholas Pratte
2024-05-28 21:04     ` Jeremy Spewock
2024-06-06 13:14     ` Juraj Linkeš
2024-05-09 11:20   ` [PATCH v2 3/8] dts: refactor EalParams Luca Vizzarro
2024-05-28 15:44     ` Nicholas Pratte
2024-05-28 21:05     ` Jeremy Spewock
2024-06-06 13:17     ` Juraj Linkeš
2024-05-09 11:20   ` [PATCH v2 4/8] dts: remove module-wide imports Luca Vizzarro
2024-05-28 15:45     ` Nicholas Pratte
2024-05-28 21:08     ` Jeremy Spewock
2024-06-06 13:21     ` Juraj Linkeš
2024-05-09 11:20   ` [PATCH v2 5/8] dts: add testpmd shell params Luca Vizzarro
2024-05-28 15:53     ` Nicholas Pratte
2024-05-28 21:05     ` Jeremy Spewock
2024-05-29 15:59       ` Luca Vizzarro
2024-05-29 17:11         ` Jeremy Spewock
2024-05-09 11:20   ` [PATCH v2 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
2024-05-28 15:49     ` Nicholas Pratte
2024-05-28 21:06       ` Jeremy Spewock
2024-05-09 11:20   ` [PATCH v2 7/8] dts: rework interactive shells Luca Vizzarro
2024-05-28 15:50     ` Nicholas Pratte
2024-05-28 21:07     ` Jeremy Spewock
2024-05-29 15:57       ` Luca Vizzarro
2024-05-09 11:20   ` [PATCH v2 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
2024-05-28 15:50     ` Nicholas Pratte
2024-05-28 21:08     ` Jeremy Spewock
2024-05-22 15:59   ` [PATCH v2 0/8] dts: add testpmd params Nicholas Pratte
2024-05-30 15:24 ` [PATCH v3 " Luca Vizzarro
2024-05-30 15:24   ` [PATCH v3 1/8] dts: add params manipulation module Luca Vizzarro
2024-05-30 20:12     ` Jeremy Spewock
2024-05-31 15:19     ` Nicholas Pratte
2024-05-30 15:24   ` [PATCH v3 2/8] dts: use Params for interactive shells Luca Vizzarro
2024-05-30 20:12     ` Jeremy Spewock
2024-05-31 15:20     ` Nicholas Pratte
2024-05-30 15:25   ` [PATCH v3 3/8] dts: refactor EalParams Luca Vizzarro
2024-05-30 20:12     ` Jeremy Spewock
2024-05-31 15:21     ` Nicholas Pratte
2024-05-30 15:25   ` [PATCH v3 4/8] dts: remove module-wide imports Luca Vizzarro
2024-05-30 20:12     ` Jeremy Spewock
2024-05-31 15:21     ` Nicholas Pratte
2024-05-30 15:25   ` [PATCH v3 5/8] dts: add testpmd shell params Luca Vizzarro
2024-05-30 20:12     ` Jeremy Spewock
2024-05-31 15:20     ` Nicholas Pratte
2024-06-06 14:37     ` Juraj Linkeš
2024-05-30 15:25   ` [PATCH v3 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
2024-05-30 20:13     ` Jeremy Spewock
2024-05-31 15:22     ` Nicholas Pratte
2024-06-06 14:38     ` Juraj Linkeš
2024-05-30 15:25   ` [PATCH v3 7/8] dts: rework interactive shells Luca Vizzarro
2024-05-30 20:13     ` Jeremy Spewock
2024-05-31 15:22     ` Nicholas Pratte
2024-06-06 18:03     ` Juraj Linkeš
2024-06-17 12:13       ` Luca Vizzarro
2024-06-18  9:18         ` Juraj Linkeš
2024-05-30 15:25   ` [PATCH v3 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
2024-05-30 20:13     ` Jeremy Spewock
2024-05-31 15:21     ` Nicholas Pratte
2024-06-06 18:05     ` Juraj Linkeš
2024-06-17 14:42 ` [PATCH v4 0/8] dts: add testpmd params and statefulness Luca Vizzarro
2024-06-17 14:42   ` [PATCH v4 1/8] dts: add params manipulation module Luca Vizzarro
2024-06-17 14:42   ` [PATCH v4 2/8] dts: use Params for interactive shells Luca Vizzarro
2024-06-17 14:42   ` [PATCH v4 3/8] dts: refactor EalParams Luca Vizzarro
2024-06-17 14:42   ` [PATCH v4 4/8] dts: remove module-wide imports Luca Vizzarro
2024-06-17 14:42   ` [PATCH v4 5/8] dts: add testpmd shell params Luca Vizzarro
2024-06-17 14:42   ` [PATCH v4 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
2024-06-17 14:42   ` [PATCH v4 7/8] dts: rework interactive shells Luca Vizzarro
2024-06-17 14:42   ` [PATCH v4 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
2024-06-17 14:54 ` [PATCH v5 0/8] dts: add testpmd params Luca Vizzarro
2024-06-17 14:54   ` [PATCH v5 1/8] dts: add params manipulation module Luca Vizzarro
2024-06-17 15:22     ` Nicholas Pratte
2024-06-17 14:54   ` [PATCH v5 2/8] dts: use Params for interactive shells Luca Vizzarro
2024-06-17 15:23     ` Nicholas Pratte
2024-06-17 14:54   ` [PATCH v5 3/8] dts: refactor EalParams Luca Vizzarro
2024-06-17 15:23     ` Nicholas Pratte
2024-06-17 14:54   ` [PATCH v5 4/8] dts: remove module-wide imports Luca Vizzarro
2024-06-17 15:23     ` Nicholas Pratte
2024-06-17 14:54   ` [PATCH v5 5/8] dts: add testpmd shell params Luca Vizzarro
2024-06-17 15:24     ` Nicholas Pratte
2024-06-17 14:54   ` [PATCH v5 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
2024-06-17 15:24     ` Nicholas Pratte
2024-06-17 14:54   ` [PATCH v5 7/8] dts: rework interactive shells Luca Vizzarro
2024-06-17 15:25     ` Nicholas Pratte
2024-06-17 14:54   ` [PATCH v5 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
2024-06-17 15:25     ` Nicholas Pratte
2024-06-19 10:23 ` [PATCH v6 0/8] dts: add testpmd params Luca Vizzarro
2024-06-19 10:23   ` [PATCH v6 1/8] dts: add params manipulation module Luca Vizzarro
2024-06-19 12:45     ` Juraj Linkeš
2024-06-19 10:23   ` [PATCH v6 2/8] dts: use Params for interactive shells Luca Vizzarro
2024-06-19 10:23   ` [PATCH v6 3/8] dts: refactor EalParams Luca Vizzarro
2024-06-19 10:23   ` [PATCH v6 4/8] dts: remove module-wide imports Luca Vizzarro
2024-06-19 10:23   ` [PATCH v6 5/8] dts: add testpmd shell params Luca Vizzarro
2024-06-19 10:23   ` [PATCH v6 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
2024-06-19 10:23   ` [PATCH v6 7/8] dts: rework interactive shells Luca Vizzarro
2024-06-19 12:49     ` Juraj Linkeš
2024-06-19 10:23   ` [PATCH v6 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
2024-06-19 14:02 ` [PATCH v7 0/8] dts: add testpmd params Luca Vizzarro
2024-06-19 14:02   ` [PATCH v7 1/8] dts: add params manipulation module Luca Vizzarro
2024-06-19 14:02   ` [PATCH v7 2/8] dts: use Params for interactive shells Luca Vizzarro
2024-06-19 14:03   ` [PATCH v7 3/8] dts: refactor EalParams Luca Vizzarro
2024-06-19 14:03   ` [PATCH v7 4/8] dts: remove module-wide imports Luca Vizzarro
2024-06-19 14:03   ` [PATCH v7 5/8] dts: add testpmd shell params Luca Vizzarro
2024-06-19 14:03   ` [PATCH v7 6/8] dts: use testpmd params for scatter test suite Luca Vizzarro
2024-06-19 14:03   ` [PATCH v7 7/8] dts: rework interactive shells Luca Vizzarro
2024-06-19 14:03   ` [PATCH v7 8/8] dts: use Unpack for type checking and hinting Luca Vizzarro
2024-06-20  3:36   ` [PATCH v7 0/8] dts: add testpmd params Thomas Monjalon

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).