From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124]) by inbox.dpdk.org (Postfix) with ESMTP id 2FA2F45C26; Thu, 31 Oct 2024 21:20:55 +0100 (CET) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id 0E7384028A; Thu, 31 Oct 2024 21:20:55 +0100 (CET) Received: from mail-lj1-f170.google.com (mail-lj1-f170.google.com [209.85.208.170]) by mails.dpdk.org (Postfix) with ESMTP id AE76840264 for ; Thu, 31 Oct 2024 21:20:53 +0100 (CET) Received: by mail-lj1-f170.google.com with SMTP id 38308e7fff4ca-2fb5034e6baso647141fa.0 for ; Thu, 31 Oct 2024 13:20:53 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=iol.unh.edu; s=unh-iol; t=1730406053; x=1731010853; darn=dpdk.org; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:from:to:cc:subject:date :message-id:reply-to; bh=m/5XyKzOh46fzCTGoFQ9PXugnEbj6rcs+Oy0IZPQ8N0=; b=eeBtB0Yi4E5GsCxYBrRmm7vvQoj0gRfrhIQOJRQHwjO0b8a0IPdbJyEduU7DiLTDZd GmNCsa+iwqDlG9BijwZ+3tGdohVv/Zo9Ywu7nZz71MyzV0ViO8WXq84XdBGUBcXggbNZ rlfyHfRz6lR8l2AguTfr5VqVFCV7j+eSrOueU= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1730406053; x=1731010853; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=m/5XyKzOh46fzCTGoFQ9PXugnEbj6rcs+Oy0IZPQ8N0=; b=UC4W9MPSGEtAOszQTe/em5I9x9lARBMjm0Q4f1wAwSvV5UpdJ7WtBKO8ccLzm/ai6a Ndx8hcUreMiLvW7bn0HO/IgFt/9KrWusZkucWxiGbbH1DmkGtrFFHkF7yjG1YzvEBm88 fPpc7ZCDrG1f5kzLU1IzeCvsEuqfWAFQSf9MsaW14LNTJeLlEjwpFzyKa9cSlify2SP4 W65Q55MYos0fo7BjUVqrpNlxw9nUNlqgcjrZW/KaaeFQLxj8ehZd3nswhd4824ZclMkh 0g60N8b0vB94a6wDTtNXm6qMbRtaF9AjtXa7qS/W0fh95oM3wHvvwXujq7sjRDpkxKcf HZGQ== X-Gm-Message-State: AOJu0YwHKQnYA4M2HrGaS2x8hwuCFwHeqIRcMR8Xo6n2fFSfBbAKRckx cdtpRRm6GW0Q1/COS/k9KB1pT1hCT6IB9eSA9egz633Bq+U9J/dHUyHwzpVfKYKyxOITioJ3JUo T6pTA/7aE9jTTIVwEuuiT3bECfuoqV7HN5JJedQ== X-Google-Smtp-Source: AGHT+IFB6G+FH19tXjJnyPkMiydR5NUavNqoDhEHyNKiLefUjXnX8pzguGdGNcUcpB+up1+DV7NZiC0HHkmzoHeh1Uk= X-Received: by 2002:a2e:be28:0:b0:2fb:5034:d0fa with SMTP id 38308e7fff4ca-2fcbdfae19emr32036741fa.4.1730406051429; Thu, 31 Oct 2024 13:20:51 -0700 (PDT) MIME-Version: 1.0 References: <20240822163941.1390326-1-luca.vizzarro@arm.com> <20241028174949.3283701-1-luca.vizzarro@arm.com> <20241028174949.3283701-5-luca.vizzarro@arm.com> In-Reply-To: <20241028174949.3283701-5-luca.vizzarro@arm.com> From: Nicholas Pratte Date: Thu, 31 Oct 2024 16:20:40 -0400 Message-ID: Subject: Re: [PATCH v4 4/8] dts: use pydantic in the configuration To: Luca Vizzarro Cc: dev@dpdk.org, Paul Szczepanek , Patrick Robb Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable X-BeenThere: dev@dpdk.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: DPDK patches and discussions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dev-bounces@dpdk.org Reviewed-by: Nicholas Pratte On Mon, Oct 28, 2024 at 1:51=E2=80=AFPM Luca Vizzarro wrote: > > This change brings in pydantic in place of warlock. Pydantic offers > a built-in model validation system in the classes, which allows for > a more resilient and simpler code. As a consequence of this change: > > - most validation is now built-in > - further validation is added to verify: > - cross referencing of node names and ports > - test suite and test cases names > - dictionaries representing the config schema are removed > - the config schema is no longer used and therefore dropped > - the TrafficGeneratorType enum has been changed from inheriting > StrEnum to the native str and Enum. This change was necessary to > enable the discriminator for object unions > - the structure of the classes has been slightly changed to perfectly > match the structure of the configuration files > - the test suite argument catches the ValidationError that > TestSuiteConfig can now raise > - the DPDK location has been wrapped under another configuration > mapping `dpdk_location` > - the DPDK locations are now structured and enforced by classes, > further simplifying the validation and handling thanks to > pattern matching > > Bugzilla ID: 1508 > > Signed-off-by: Luca Vizzarro > Reviewed-by: Paul Szczepanek > --- > doc/api/dts/conf_yaml_schema.json | 1 - > doc/api/dts/framework.config.rst | 6 - > doc/api/dts/framework.config.types.rst | 8 - > dts/conf.yaml | 11 +- > dts/framework/config/__init__.py | 822 +++++++++--------- > dts/framework/config/conf_yaml_schema.json | 459 ---------- > dts/framework/config/types.py | 149 ---- > dts/framework/runner.py | 57 +- > dts/framework/settings.py | 124 +-- > dts/framework/testbed_model/node.py | 15 +- > dts/framework/testbed_model/os_session.py | 4 +- > dts/framework/testbed_model/port.py | 4 +- > dts/framework/testbed_model/posix_session.py | 10 +- > dts/framework/testbed_model/sut_node.py | 182 ++-- > dts/framework/testbed_model/topology.py | 11 +- > .../traffic_generator/__init__.py | 4 +- > .../traffic_generator/traffic_generator.py | 2 +- > dts/framework/utils.py | 2 +- > dts/tests/TestSuite_smoke_tests.py | 2 +- > 19 files changed, 653 insertions(+), 1220 deletions(-) > delete mode 120000 doc/api/dts/conf_yaml_schema.json > delete mode 100644 doc/api/dts/framework.config.types.rst > delete mode 100644 dts/framework/config/conf_yaml_schema.json > delete mode 100644 dts/framework/config/types.py > > diff --git a/doc/api/dts/conf_yaml_schema.json b/doc/api/dts/conf_yaml_sc= hema.json > deleted file mode 120000 > index 5978642d76..0000000000 > --- a/doc/api/dts/conf_yaml_schema.json > +++ /dev/null > @@ -1 +0,0 @@ > -../../../dts/framework/config/conf_yaml_schema.json > \ No newline at end of file > diff --git a/doc/api/dts/framework.config.rst b/doc/api/dts/framework.con= fig.rst > index 261997aefa..cc266276c1 100644 > --- a/doc/api/dts/framework.config.rst > +++ b/doc/api/dts/framework.config.rst > @@ -6,9 +6,3 @@ config - Configuration Package > .. automodule:: framework.config > :members: > :show-inheritance: > - > -.. toctree:: > - :hidden: > - :maxdepth: 1 > - > - framework.config.types > diff --git a/doc/api/dts/framework.config.types.rst b/doc/api/dts/framewo= rk.config.types.rst > deleted file mode 100644 > index a50a0c874a..0000000000 > --- a/doc/api/dts/framework.config.types.rst > +++ /dev/null > @@ -1,8 +0,0 @@ > -.. SPDX-License-Identifier: BSD-3-Clause > - > -config.types - Configuration Types > -=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D > - > -.. automodule:: framework.config.types > - :members: > - :show-inheritance: > diff --git a/dts/conf.yaml b/dts/conf.yaml > index 8a65a481d6..2496262854 100644 > --- a/dts/conf.yaml > +++ b/dts/conf.yaml > @@ -5,11 +5,12 @@ > test_runs: > # define one test run environment > - dpdk_build: > - # dpdk_tree: Commented out because `tarball` is defined. > - tarball: dpdk-tarball.tar.xz > - # Either `dpdk_tree` or `tarball` can be defined, but not both. > - remote: false # Optional, defaults to false. If it's true, the `dp= dk_tree` or `tarball` > - # is located on the SUT node, instead of the executi= on host. > + dpdk_location: > + # dpdk_tree: Commented out because `tarball` is defined. > + tarball: dpdk-tarball.tar.xz > + # Either `dpdk_tree` or `tarball` can be defined, but not both. > + remote: false # Optional, defaults to false. If it's true, the `= dpdk_tree` or `tarball` > + # is located on the SUT node, instead of the execu= tion host. > > # precompiled_build_dir: Commented out because `build_options` is = defined. > build_options: > diff --git a/dts/framework/config/__init__.py b/dts/framework/config/__in= it__.py > index 7403ccbf14..c86bfaaabf 100644 > --- a/dts/framework/config/__init__.py > +++ b/dts/framework/config/__init__.py > @@ -2,17 +2,18 @@ > # Copyright(c) 2010-2021 Intel Corporation > # Copyright(c) 2022-2023 University of New Hampshire > # Copyright(c) 2023 PANTHEON.tech s.r.o. > +# Copyright(c) 2024 Arm Limited > > """Testbed configuration and test suite specification. > > This package offers classes that hold real-time information about the te= stbed, hold test run > configuration describing the tested testbed and a loader function, :func= :`load_config`, which loads > -the YAML test run configuration file > -and validates it according to :download:`the schema `. > +the YAML test run configuration file and validates it against the :class= :`Configuration` Pydantic > +model. > > The YAML test run configuration file is parsed into a dictionary, parts = of which are used throughout > -this package. The allowed keys and types inside this dictionary are defi= ned in > -the :doc:`types ` module. > +this package. The allowed keys and types inside this dictionary map dire= ctly to the > +:class:`Configuration` model, its fields and sub-models. > > The test run configuration has two main sections: > > @@ -24,39 +25,28 @@ > > The real-time information about testbed is supposed to be gathered at ru= ntime. > > -The classes defined in this package make heavy use of :mod:`dataclasses`= . > -All of them use slots and are frozen: > +The classes defined in this package make heavy use of :mod:`pydantic`. > +Nearly all of them are frozen: > > - * Slots enables some optimizations, by pre-allocating space for the = defined > - attributes in the underlying data structure, > * Frozen makes the object immutable. This enables further optimizati= ons, > and makes it thread safe should we ever want to move in that direc= tion. > """ > > -import json > -import os.path > import tarfile > -from dataclasses import dataclass, fields > -from enum import auto, unique > -from pathlib import Path > -from typing import Union > +from enum import Enum, auto, unique > +from functools import cached_property > +from pathlib import Path, PurePath > +from typing import TYPE_CHECKING, Annotated, Any, Literal, NamedTuple > > -import warlock # type: ignore[import-untyped] > import yaml > +from pydantic import BaseModel, Field, ValidationError, field_validator,= model_validator > from typing_extensions import Self > > -from framework.config.types import ( > - ConfigurationDict, > - DPDKBuildConfigDict, > - DPDKConfigurationDict, > - NodeConfigDict, > - PortConfigDict, > - TestRunConfigDict, > - TestSuiteConfigDict, > - TrafficGeneratorConfigDict, > -) > from framework.exception import ConfigurationError > -from framework.utils import StrEnum > +from framework.utils import REGEX_FOR_PCI_ADDRESS, StrEnum > + > +if TYPE_CHECKING: > + from framework.test_suite import TestSuiteSpec > > > @unique > @@ -118,15 +108,14 @@ class Compiler(StrEnum): > > > @unique > -class TrafficGeneratorType(StrEnum): > +class TrafficGeneratorType(str, Enum): > """The supported traffic generators.""" > > #: > - SCAPY =3D auto() > + SCAPY =3D "SCAPY" > > > -@dataclass(slots=3DTrue, frozen=3DTrue) > -class HugepageConfiguration: > +class HugepageConfiguration(BaseModel, frozen=3DTrue, extra=3D"forbid"): > r"""The hugepage configuration of :class:`~framework.testbed_model.n= ode.Node`\s. > > Attributes: > @@ -138,12 +127,10 @@ class HugepageConfiguration: > force_first_numa: bool > > > -@dataclass(slots=3DTrue, frozen=3DTrue) > -class PortConfig: > +class PortConfig(BaseModel, frozen=3DTrue, extra=3D"forbid"): > r"""The port configuration of :class:`~framework.testbed_model.node.= Node`\s. > > Attributes: > - node: The :class:`~framework.testbed_model.node.Node` where this= port exists. > pci: The PCI address of the port. > os_driver_for_dpdk: The operating system driver name for use wit= h DPDK. > os_driver: The operating system driver name when the operating s= ystem controls the port. > @@ -152,70 +139,57 @@ class PortConfig: > peer_pci: The PCI address of the port connected to this port. > """ > > - node: str > - pci: str > - os_driver_for_dpdk: str > - os_driver: str > - peer_node: str > - peer_pci: str > - > - @classmethod > - def from_dict(cls, node: str, d: PortConfigDict) -> Self: > - """A convenience method that creates the object from fewer input= s. > - > - Args: > - node: The node where this port exists. > - d: The configuration dictionary. > - > - Returns: > - The port configuration instance. > - """ > - return cls(node=3Dnode, **d) > - > - > -@dataclass(slots=3DTrue, frozen=3DTrue) > -class TrafficGeneratorConfig: > - """The configuration of traffic generators. > - > - The class will be expanded when more configuration is needed. > + pci: str =3D Field( > + description=3D"The local PCI address of the port.", pattern=3DRE= GEX_FOR_PCI_ADDRESS > + ) > + os_driver_for_dpdk: str =3D Field( > + description=3D"The driver that the kernel should bind this devic= e to for DPDK to use it.", > + examples=3D["vfio-pci", "mlx5_core"], > + ) > + os_driver: str =3D Field( > + description=3D"The driver normally used by this port", examples= =3D["i40e", "ice", "mlx5_core"] > + ) > + peer_node: str =3D Field(description=3D"The name of the peer node th= is port is connected to.") > + peer_pci: str =3D Field( > + description=3D"The PCI address of the peer port this port is con= nected to.", > + pattern=3DREGEX_FOR_PCI_ADDRESS, > + ) > + > + > +class TrafficGeneratorConfig(BaseModel, frozen=3DTrue, extra=3D"forbid")= : > + """A protocol required to define traffic generator types. > > Attributes: > - traffic_generator_type: The type of the traffic generator. > + type: The traffic generator type, the child class is required to= define to be distinguished > + among others. > """ > > - traffic_generator_type: TrafficGeneratorType > + type: TrafficGeneratorType > > - @staticmethod > - def from_dict(d: TrafficGeneratorConfigDict) -> "TrafficGeneratorCon= fig": > - """A convenience method that produces traffic generator config o= f the proper type. > > - Args: > - d: The configuration dictionary. > +class ScapyTrafficGeneratorConfig(TrafficGeneratorConfig, frozen=3DTrue,= extra=3D"forbid"): > + """Scapy traffic generator specific configuration.""" > > - Returns: > - The traffic generator configuration instance. > + type: Literal[TrafficGeneratorType.SCAPY] > > - Raises: > - ConfigurationError: An unknown traffic generator type was en= countered. > - """ > - match TrafficGeneratorType(d["type"]): > - case TrafficGeneratorType.SCAPY: > - return ScapyTrafficGeneratorConfig( > - traffic_generator_type=3DTrafficGeneratorType.SCAPY > - ) > - case _: > - raise ConfigurationError(f'Unknown traffic generator typ= e "{d["type"]}".') > > +#: A union type discriminating traffic generators by the `type` field. > +TrafficGeneratorConfigTypes =3D Annotated[ScapyTrafficGeneratorConfig, F= ield(discriminator=3D"type")] > > -@dataclass(slots=3DTrue, frozen=3DTrue) > -class ScapyTrafficGeneratorConfig(TrafficGeneratorConfig): > - """Scapy traffic generator specific configuration.""" > > - pass > +#: A field representing logical core ranges. > +LogicalCores =3D Annotated[ > + str, > + Field( > + description=3D"Comma-separated list of logical cores to use. " > + "An empty string means use all lcores.", > + examples=3D["1,2,3,4,5,18-22", "10-15"], > + pattern=3Dr"^(([0-9]+|([0-9]+-[0-9]+))(,([0-9]+|([0-9]+-[0-9]+))= )*)?$", > + ), > +] > > > -@dataclass(slots=3DTrue, frozen=3DTrue) > -class NodeConfiguration: > +class NodeConfiguration(BaseModel, frozen=3DTrue, extra=3D"forbid"): > r"""The configuration of :class:`~framework.testbed_model.node.Node`= \s. > > Attributes: > @@ -234,285 +208,317 @@ class NodeConfiguration: > ports: The ports that can be used in testing. > """ > > - name: str > - hostname: str > - user: str > - password: str | None > + name: str =3D Field(description=3D"A unique identifier for this node= .") > + hostname: str =3D Field(description=3D"The hostname or IP address of= the node.") > + user: str =3D Field(description=3D"The login user to use to connect = to this node.") > + password: str | None =3D Field( > + default=3DNone, > + description=3D"The login password to use to connect to this node= . " > + "SSH keys are STRONGLY preferred, use only as last resort.", > + ) > arch: Architecture > os: OS > - lcores: str > - use_first_core: bool > - hugepages: HugepageConfiguration | None > - ports: list[PortConfig] > - > - @staticmethod > - def from_dict( > - d: NodeConfigDict, > - ) -> Union["SutNodeConfiguration", "TGNodeConfiguration"]: > - """A convenience method that processes the inputs before creatin= g a specialized instance. > - > - Args: > - d: The configuration dictionary. > - > - Returns: > - Either an SUT or TG configuration instance. > - """ > - hugepage_config =3D None > - if "hugepages_2mb" in d: > - hugepage_config_dict =3D d["hugepages_2mb"] > - if "force_first_numa" not in hugepage_config_dict: > - hugepage_config_dict["force_first_numa"] =3D False > - hugepage_config =3D HugepageConfiguration(**hugepage_config_= dict) > - > - # The calls here contain duplicated code which is here because M= ypy doesn't > - # properly support dictionary unpacking with TypedDicts > - if "traffic_generator" in d: > - return TGNodeConfiguration( > - name=3Dd["name"], > - hostname=3Dd["hostname"], > - user=3Dd["user"], > - password=3Dd.get("password"), > - arch=3DArchitecture(d["arch"]), > - os=3DOS(d["os"]), > - lcores=3Dd.get("lcores", "1"), > - use_first_core=3Dd.get("use_first_core", False), > - hugepages=3Dhugepage_config, > - ports=3D[PortConfig.from_dict(d["name"], port) for port = in d["ports"]], > - traffic_generator=3DTrafficGeneratorConfig.from_dict(d["= traffic_generator"]), > - ) > - else: > - return SutNodeConfiguration( > - name=3Dd["name"], > - hostname=3Dd["hostname"], > - user=3Dd["user"], > - password=3Dd.get("password"), > - arch=3DArchitecture(d["arch"]), > - os=3DOS(d["os"]), > - lcores=3Dd.get("lcores", "1"), > - use_first_core=3Dd.get("use_first_core", False), > - hugepages=3Dhugepage_config, > - ports=3D[PortConfig.from_dict(d["name"], port) for port = in d["ports"]], > - memory_channels=3Dd.get("memory_channels", 1), > - ) > + lcores: LogicalCores =3D "1" > + use_first_core: bool =3D Field( > + default=3DFalse, description=3D"DPDK won't use the first physica= l core if set to False." > + ) > + hugepages: HugepageConfiguration | None =3D Field(None, alias=3D"hug= epages_2mb") > + ports: list[PortConfig] =3D Field(min_length=3D1) > > > -@dataclass(slots=3DTrue, frozen=3DTrue) > -class SutNodeConfiguration(NodeConfiguration): > +class SutNodeConfiguration(NodeConfiguration, frozen=3DTrue, extra=3D"fo= rbid"): > """:class:`~framework.testbed_model.sut_node.SutNode` specific confi= guration. > > Attributes: > memory_channels: The number of memory channels to use when runni= ng DPDK. > """ > > - memory_channels: int > + memory_channels: int =3D Field( > + default=3D1, description=3D"Number of memory channels to use whe= n running DPDK." > + ) > > > -@dataclass(slots=3DTrue, frozen=3DTrue) > -class TGNodeConfiguration(NodeConfiguration): > +class TGNodeConfiguration(NodeConfiguration, frozen=3DTrue, extra=3D"for= bid"): > """:class:`~framework.testbed_model.tg_node.TGNode` specific configu= ration. > > Attributes: > traffic_generator: The configuration of the traffic generator pr= esent on the TG node. > """ > > - traffic_generator: TrafficGeneratorConfig > + traffic_generator: TrafficGeneratorConfigTypes > + > + > +#: Union type for all the node configuration types. > +NodeConfigurationTypes =3D TGNodeConfiguration | SutNodeConfiguration > > > -@dataclass(slots=3DTrue, frozen=3DTrue) > -class DPDKBuildConfiguration: > - """DPDK build configuration. > +def resolve_path(path: Path) -> Path: > + """Resolve a path into a real path.""" > + return path.resolve() > > - The configuration used for building DPDK. > + > +class BaseDPDKLocation(BaseModel, frozen=3DTrue, extra=3D"forbid"): > + """DPDK location. > + > + The path to the DPDK sources, build dir and type of location. > > Attributes: > - arch: The target architecture to build for. > - os: The target os to build for. > - cpu: The target CPU to build for. > - compiler: The compiler executable to use. > - compiler_wrapper: This string will be put in front of the compil= er when > - executing the build. Useful for adding wrapper commands, suc= h as ``ccache``. > - name: The name of the compiler. > + remote: Optional, defaults to :data:`False`. If :data:`True`, `d= pdk_tree` or `tarball` is > + located on the SUT node, instead of the execution host. > """ > > - arch: Architecture > - os: OS > - cpu: CPUType > - compiler: Compiler > - compiler_wrapper: str > - name: str > + remote: bool =3D False > > - @classmethod > - def from_dict(cls, d: DPDKBuildConfigDict) -> Self: > - r"""A convenience method that processes the inputs before creati= ng an instance. > > - `arch`, `os`, `cpu` and `compiler` are converted to :class:`Enum= `\s and > - `name` is constructed from `arch`, `os`, `cpu` and `compiler`. > +class LocalDPDKLocation(BaseDPDKLocation, frozen=3DTrue, extra=3D"forbid= "): > + """Local DPDK location parent class. > > - Args: > - d: The configuration dictionary. > + This class is meant to represent any location that is present only l= ocally. > + """ > > - Returns: > - The DPDK build configuration instance. > - """ > - return cls( > - arch=3DArchitecture(d["arch"]), > - os=3DOS(d["os"]), > - cpu=3DCPUType(d["cpu"]), > - compiler=3DCompiler(d["compiler"]), > - compiler_wrapper=3Dd.get("compiler_wrapper", ""), > - name=3Df"{d['arch']}-{d['os']}-{d['cpu']}-{d['compiler']}", > - ) > + remote: Literal[False] =3D False > > > -@dataclass(slots=3DTrue, frozen=3DTrue) > -class DPDKLocation: > - """DPDK location. > +class LocalDPDKTreeLocation(LocalDPDKLocation, frozen=3DTrue, extra=3D"f= orbid"): > + """Local DPDK tree location. > > - The path to the DPDK sources, build dir and type of location. > + This class makes a distinction from :class:`RemoteDPDKTreeLocation` = by enforcing on the fly > + validation. > > Attributes: > - dpdk_tree: The path to the DPDK source tree directory. Only one = of `dpdk_tree` or `tarball` > - must be provided. > - tarball: The path to the DPDK tarball. Only one of `dpdk_tree` o= r `tarball` must be > - provided. > - remote: Optional, defaults to :data:`False`. If :data:`True`, `d= pdk_tree` or `tarball` is > - located on the SUT node, instead of the execution host. > - build_dir: If it's defined, DPDK has been pre-compiled and the b= uild directory is located in > - a subdirectory of `dpdk_tree` or `tarball` root directory. O= therwise, will be using > - `build_options` from configuration to build the DPDK from so= urce. > + dpdk_tree: The path to the DPDK source tree directory. > """ > > - dpdk_tree: str | None > - tarball: str | None > - remote: bool > - build_dir: str | None > + dpdk_tree: Path > > - @classmethod > - def from_dict(cls, d: DPDKConfigurationDict) -> Self: > - """A convenience method that processes and validates the inputs = before creating an instance. > + #: Resolve the local DPDK tree path > + resolve_dpdk_tree_path =3D field_validator("dpdk_tree")(resolve_path= ) > > - Validate existence and format of `dpdk_tree` or `tarball` on loc= al filesystem, if > - `remote` is False. > + @model_validator(mode=3D"after") > + def validate_dpdk_tree_path(self) -> Self: > + """Validate the provided DPDK tree path.""" > + assert self.dpdk_tree.exists(), "DPDK tree not found in local fi= lesystem." > + assert self.dpdk_tree.is_dir(), "The DPDK tree path must be a di= rectory." > + return self > > - Args: > - d: The configuration dictionary. > > - Returns: > - The DPDK location instance. > +class LocalDPDKTarballLocation(LocalDPDKLocation, frozen=3DTrue, extra= =3D"forbid"): > + """Local DPDK tarball location. > > - Raises: > - ConfigurationError: If `dpdk_tree` or `tarball` not found in= local filesystem or they > - aren't in the right format. > - """ > - dpdk_tree =3D d.get("dpdk_tree") > - tarball =3D d.get("tarball") > - remote =3D d.get("remote", False) > - > - if not remote: > - if dpdk_tree: > - if not Path(dpdk_tree).exists(): > - raise ConfigurationError( > - f"DPDK tree '{dpdk_tree}' not found in local fil= esystem." > - ) > - > - if not Path(dpdk_tree).is_dir(): > - raise ConfigurationError(f"The DPDK tree '{dpdk_tree= }' must be a directory.") > - > - dpdk_tree =3D os.path.realpath(dpdk_tree) > - > - if tarball: > - if not Path(tarball).exists(): > - raise ConfigurationError( > - f"DPDK tarball '{tarball}' not found in local fi= lesystem." > - ) > - > - if not tarfile.is_tarfile(tarball): > - raise ConfigurationError( > - f"The DPDK tarball '{tarball}' must be a valid t= ar archive." > - ) > - > - return cls( > - dpdk_tree=3Ddpdk_tree, > - tarball=3Dtarball, > - remote=3Dremote, > - build_dir=3Dd.get("precompiled_build_dir"), > - ) > + This class makes a distinction from :class:`RemoteDPDKTarballLocatio= n` by enforcing on the fly > + validation. > + > + Attributes: > + tarball: The path to the DPDK tarball. > + """ > > + tarball: Path > > -@dataclass > -class DPDKConfiguration: > - """The configuration of the DPDK build. > + #: Resolve the local tarball path > + resolve_tarball_path =3D field_validator("tarball")(resolve_path) > > - The configuration contain the location of the DPDK and configuration= used for > - building it. > + @model_validator(mode=3D"after") > + def validate_tarball_path(self) -> Self: > + """Validate the provided tarball.""" > + assert self.tarball.exists(), "DPDK tarball not found in local f= ilesystem." > + assert tarfile.is_tarfile(self.tarball), "The DPDK tarball must = be a valid tar archive." > + return self > + > + > +class RemoteDPDKLocation(BaseDPDKLocation, frozen=3DTrue, extra=3D"forbi= d"): > + """Remote DPDK location parent class. > + > + This class is meant to represent any location that is present only r= emotely. > + """ > + > + remote: Literal[True] =3D True > + > + > +class RemoteDPDKTreeLocation(RemoteDPDKLocation, frozen=3DTrue, extra=3D= "forbid"): > + """Remote DPDK tree location. > + > + This class is distinct from :class:`LocalDPDKTreeLocation` which enf= orces on the fly validation. > + > + Attributes: > + dpdk_tree: The path to the DPDK source tree directory. > + """ > + > + dpdk_tree: PurePath > + > + > +class RemoteDPDKTarballLocation(LocalDPDKLocation, frozen=3DTrue, extra= =3D"forbid"): > + """Remote DPDK tarball location. > + > + This class is distinct from :class:`LocalDPDKTarballLocation` which = enforces on the fly > + validation. > + > + Attributes: > + tarball: The path to the DPDK tarball. > + """ > + > + tarball: PurePath > + > + > +#: Union type for different DPDK locations > +DPDKLocation =3D ( > + LocalDPDKTreeLocation > + | LocalDPDKTarballLocation > + | RemoteDPDKTreeLocation > + | RemoteDPDKTarballLocation > +) > + > + > +class BaseDPDKBuildConfiguration(BaseModel, frozen=3DTrue, extra=3D"forb= id"): > + """The base configuration for different types of build. > + > + The configuration contain the location of the DPDK and configuration= used for building it. > > Attributes: > dpdk_location: The location of the DPDK tree. > - dpdk_build_config: A DPDK build configuration to test. If :data:= `None`, > - DTS will use pre-built DPDK from `build_dir` in a :class:`DP= DKLocation`. > """ > > dpdk_location: DPDKLocation > - dpdk_build_config: DPDKBuildConfiguration | None > > - @classmethod > - def from_dict(cls, d: DPDKConfigurationDict) -> Self: > - """A convenience method that processes the inputs before creatin= g an instance. > > - Args: > - d: The configuration dictionary. > +class DPDKPrecompiledBuildConfiguration(BaseDPDKBuildConfiguration, froz= en=3DTrue, extra=3D"forbid"): > + """DPDK precompiled build configuration. > > - Returns: > - The DPDK configuration. > - """ > - return cls( > - dpdk_location=3DDPDKLocation.from_dict(d), > - dpdk_build_config=3D( > - DPDKBuildConfiguration.from_dict(d["build_options"]) > - if d.get("build_options") > - else None > - ), > - ) > + Attributes: > + precompiled_build_dir: If it's defined, DPDK has been pre-compil= ed and the build directory > + is located in a subdirectory of `dpdk_tree` or `tarball` roo= t directory. Otherwise, will > + be using `dpdk_build_config` from configuration to build the= DPDK from source. > + """ > + > + precompiled_build_dir: str =3D Field(min_length=3D1) > + > + > +class DPDKBuildOptionsConfiguration(BaseModel, frozen=3DTrue, extra=3D"f= orbid"): > + """DPDK build options configuration. > + > + The build options used for building DPDK. > + > + Attributes: > + arch: The target architecture to build for. > + os: The target os to build for. > + cpu: The target CPU to build for. > + compiler: The compiler executable to use. > + compiler_wrapper: This string will be put in front of the compil= er when executing the build. > + Useful for adding wrapper commands, such as ``ccache``. > + """ > + > + arch: Architecture > + os: OS > + cpu: CPUType > + compiler: Compiler > + compiler_wrapper: str =3D "" > > + @cached_property > + def name(self) -> str: > + """The name of the compiler.""" > + return f"{self.arch}-{self.os}-{self.cpu}-{self.compiler}" > > -@dataclass(slots=3DTrue, frozen=3DTrue) > -class TestSuiteConfig: > + > +class DPDKUncompiledBuildConfiguration(BaseDPDKBuildConfiguration, froze= n=3DTrue, extra=3D"forbid"): > + """DPDK uncompiled build configuration. > + > + Attributes: > + build_options: The build options to compile DPDK. > + """ > + > + build_options: DPDKBuildOptionsConfiguration > + > + > +#: Union type for different build configurations > +DPDKBuildConfiguration =3D DPDKPrecompiledBuildConfiguration | DPDKUncom= piledBuildConfiguration > + > + > +class TestSuiteConfig(BaseModel, frozen=3DTrue, extra=3D"forbid"): > """Test suite configuration. > > - Information about a single test suite to be executed. > + Information about a single test suite to be executed. This can also = be represented as a string > + instead of a mapping, example: > + > + .. code:: yaml > + > + test_runs: > + - test_suites: > + # As string representation: > + - hello_world # test all of `hello_world`, or > + - hello_world hello_world_single_core # test only `hello_wor= ld_single_core` > + # or as model fields: > + - test_suite: hello_world > + test_cases: [hello_world_single_core] # without this field= all test cases are run > > Attributes: > - test_suite: The name of the test suite module without the starti= ng ``TestSuite_``. > - test_cases: The names of test cases from this test suite to exec= ute. > + test_suite_name: The name of the test suite module without the s= tarting ``TestSuite_``. > + test_cases_names: The names of test cases from this test suite t= o execute. > If empty, all test cases will be executed. > """ > > - test_suite: str > - test_cases: list[str] > - > + test_suite_name: str =3D Field( > + title=3D"Test suite name", > + description=3D"The identifying module name of the test suite wit= hout the prefix.", > + alias=3D"test_suite", > + ) > + test_cases_names: list[str] =3D Field( > + default_factory=3Dlist, > + title=3D"Test cases by name", > + description=3D"The identifying name of the test cases of the tes= t suite.", > + alias=3D"test_cases", > + ) > + > + @cached_property > + def test_suite_spec(self) -> "TestSuiteSpec": > + """The specification of the requested test suite.""" > + from framework.test_suite import find_by_name > + > + test_suite_spec =3D find_by_name(self.test_suite_name) > + assert ( > + test_suite_spec is not None > + ), f"{self.test_suite_name} is not a valid test suite module nam= e." > + return test_suite_spec > + > + @model_validator(mode=3D"before") > @classmethod > - def from_dict( > - cls, > - entry: str | TestSuiteConfigDict, > - ) -> Self: > - """Create an instance from two different types. > + def convert_from_string(cls, data: Any) -> Any: > + """Convert the string representation of the model into a valid m= apping.""" > + if isinstance(data, str): > + [test_suite, *test_cases] =3D data.split() > + return dict(test_suite=3Dtest_suite, test_cases=3Dtest_cases= ) > + return data > + > + @model_validator(mode=3D"after") > + def validate_names(self) -> Self: > + """Validate the supplied test suite and test cases names. > + > + This validator relies on the cached property `test_suite_spec` t= o run for the first > + time in this call, therefore triggering the assertions if needed= . > + """ > + available_test_cases =3D map( > + lambda t: t.name, self.test_suite_spec.class_obj.get_test_ca= ses() > + ) > + for requested_test_case in self.test_cases_names: > + assert requested_test_case in available_test_cases, ( > + f"{requested_test_case} is not a valid test case " > + f"of test suite {self.test_suite_name}." > + ) > > - Args: > - entry: Either a suite name or a dictionary containing the co= nfig. > + return self > > - Returns: > - The test suite configuration instance. > - """ > - if isinstance(entry, str): > - return cls(test_suite=3Dentry, test_cases=3D[]) > - elif isinstance(entry, dict): > - return cls(test_suite=3Dentry["suite"], test_cases=3Dentry["= cases"]) > - else: > - raise TypeError(f"{type(entry)} is not valid for a test suit= e config.") > > +class TestRunSUTNodeConfiguration(BaseModel, frozen=3DTrue, extra=3D"for= bid"): > + """The SUT node configuration of a test run. > > -@dataclass(slots=3DTrue, frozen=3DTrue) > -class TestRunConfiguration: > + Attributes: > + node_name: The SUT node to use in this test run. > + vdevs: The names of virtual devices to test. > + """ > + > + node_name: str > + vdevs: list[str] =3D Field(default_factory=3Dlist) > + > + > +class TestRunConfiguration(BaseModel, frozen=3DTrue, extra=3D"forbid"): > """The configuration of a test run. > > The configuration contains testbed information, what tests to execut= e > @@ -524,144 +530,130 @@ class TestRunConfiguration: > func: Whether to run functional tests. > skip_smoke_tests: Whether to skip smoke tests. > test_suites: The names of test suites and/or test cases to execu= te. > - system_under_test_node: The SUT node to use in this test run. > - traffic_generator_node: The TG node to use in this test run. > - vdevs: The names of virtual devices to test. > + system_under_test_node: The SUT node configuration to use in thi= s test run. > + traffic_generator_node: The TG node name to use in this test run= . > random_seed: The seed to use for pseudo-random generation. > """ > > - dpdk_config: DPDKConfiguration > - perf: bool > - func: bool > - skip_smoke_tests: bool > - test_suites: list[TestSuiteConfig] > - system_under_test_node: SutNodeConfiguration > - traffic_generator_node: TGNodeConfiguration > - vdevs: list[str] > - random_seed: int | None > - > - @classmethod > - def from_dict( > - cls, > - d: TestRunConfigDict, > - node_map: dict[str, SutNodeConfiguration | TGNodeConfiguration], > - ) -> Self: > - """A convenience method that processes the inputs before creatin= g an instance. > - > - The DPDK build and the test suite config are transformed into th= eir respective objects. > - SUT and TG configurations are taken from `node_map`. The other (= :class:`bool`) attributes > - are just stored. > - > - Args: > - d: The test run configuration dictionary. > - node_map: A dictionary mapping node names to their config ob= jects. > - > - Returns: > - The test run configuration instance. > - """ > - test_suites: list[TestSuiteConfig] =3D list(map(TestSuiteConfig.= from_dict, d["test_suites"])) > - sut_name =3D d["system_under_test_node"]["node_name"] > - skip_smoke_tests =3D d.get("skip_smoke_tests", False) > - assert sut_name in node_map, f"Unknown SUT {sut_name} in test ru= n {d}" > - system_under_test_node =3D node_map[sut_name] > - assert isinstance( > - system_under_test_node, SutNodeConfiguration > - ), f"Invalid SUT configuration {system_under_test_node}" > - > - tg_name =3D d["traffic_generator_node"] > - assert tg_name in node_map, f"Unknown TG {tg_name} in test run {= d}" > - traffic_generator_node =3D node_map[tg_name] > - assert isinstance( > - traffic_generator_node, TGNodeConfiguration > - ), f"Invalid TG configuration {traffic_generator_node}" > - > - vdevs =3D ( > - d["system_under_test_node"]["vdevs"] if "vdevs" in d["system= _under_test_node"] else [] > - ) > - random_seed =3D d.get("random_seed", None) > - return cls( > - dpdk_config=3DDPDKConfiguration.from_dict(d["dpdk_build"]), > - perf=3Dd["perf"], > - func=3Dd["func"], > - skip_smoke_tests=3Dskip_smoke_tests, > - test_suites=3Dtest_suites, > - system_under_test_node=3Dsystem_under_test_node, > - traffic_generator_node=3Dtraffic_generator_node, > - vdevs=3Dvdevs, > - random_seed=3Drandom_seed, > - ) > - > - def copy_and_modify(self, **kwargs) -> Self: > - """Create a shallow copy with any of the fields modified. > + dpdk_config: DPDKBuildConfiguration =3D Field(alias=3D"dpdk_build") > + perf: bool =3D Field(description=3D"Enable performance testing.") > + func: bool =3D Field(description=3D"Enable functional testing.") > + skip_smoke_tests: bool =3D False > + test_suites: list[TestSuiteConfig] =3D Field(min_length=3D1) > + system_under_test_node: TestRunSUTNodeConfiguration > + traffic_generator_node: str > + random_seed: int | None =3D None > > - The only new data are those passed to this method. > - The rest are copied from the object's fields calling the method. > > - Args: > - **kwargs: The names and types of keyword arguments are defin= ed > - by the fields of the :class:`TestRunConfiguration` class= . > +class TestRunWithNodesConfiguration(NamedTuple): > + """Tuple containing the configuration of the test run and its associ= ated nodes.""" > > - Returns: > - The copied and modified test run configuration. > - """ > - new_config =3D {} > - for field in fields(self): > - if field.name in kwargs: > - new_config[field.name] =3D kwargs[field.name] > - else: > - new_config[field.name] =3D getattr(self, field.name) > - > - return type(self)(**new_config) > + #: > + test_run_config: TestRunConfiguration > + #: > + sut_node_config: SutNodeConfiguration > + #: > + tg_node_config: TGNodeConfiguration > > > -@dataclass(slots=3DTrue, frozen=3DTrue) > -class Configuration: > +class Configuration(BaseModel, extra=3D"forbid"): > """DTS testbed and test configuration. > > - The node configuration is not stored in this object. Rather, all use= d node configurations > - are stored inside the test run configuration where the nodes are act= ually used. > - > Attributes: > test_runs: Test run configurations. > + nodes: Node configurations. > """ > > - test_runs: list[TestRunConfiguration] > + test_runs: list[TestRunConfiguration] =3D Field(min_length=3D1) > + nodes: list[NodeConfigurationTypes] =3D Field(min_length=3D1) > > - @classmethod > - def from_dict(cls, d: ConfigurationDict) -> Self: > - """A convenience method that processes the inputs before creatin= g an instance. > + @cached_property > + def test_runs_with_nodes(self) -> list[TestRunWithNodesConfiguration= ]: > + """List of test runs with the associated nodes.""" > + test_runs_with_nodes =3D [] > > - DPDK build and test suite config are transformed into their resp= ective objects. > - SUT and TG configurations are taken from `node_map`. The other (= :class:`bool`) attributes > - are just stored. > + for test_run_no, test_run in enumerate(self.test_runs): > + sut_node_name =3D test_run.system_under_test_node.node_name > + sut_node =3D next(filter(lambda n: n.name =3D=3D sut_node_na= me, self.nodes), None) > > - Args: > - d: The configuration dictionary. > + assert sut_node is not None, ( > + f"test_runs.{test_run_no}.sut_node_config.node_name " > + f"({test_run.system_under_test_node.node_name}) is not a= valid node name" > + ) > + assert isinstance(sut_node, SutNodeConfiguration), ( > + f"test_runs.{test_run_no}.sut_node_config.node_name is a= valid node name, " > + "but it is not a valid SUT node" > + ) > > - Returns: > - The whole configuration instance. > - """ > - nodes: list[SutNodeConfiguration | TGNodeConfiguration] =3D list= ( > - map(NodeConfiguration.from_dict, d["nodes"]) > - ) > - assert len(nodes) > 0, "There must be a node to test" > + tg_node_name =3D test_run.traffic_generator_node > + tg_node =3D next(filter(lambda n: n.name =3D=3D tg_node_name= , self.nodes), None) > > - node_map =3D {node.name: node for node in nodes} > - assert len(nodes) =3D=3D len(node_map), "Duplicate node names ar= e not allowed" > + assert tg_node is not None, ( > + f"test_runs.{test_run_no}.tg_node_name " > + f"({test_run.traffic_generator_node}) is not a valid nod= e name" > + ) > + assert isinstance(tg_node, TGNodeConfiguration), ( > + f"test_runs.{test_run_no}.tg_node_name is a valid node n= ame, " > + "but it is not a valid TG node" > + ) > > - test_runs: list[TestRunConfiguration] =3D list( > - map(TestRunConfiguration.from_dict, d["test_runs"], [node_ma= p for _ in d]) > - ) > + test_runs_with_nodes.append(TestRunWithNodesConfiguration(te= st_run, sut_node, tg_node)) > + > + return test_runs_with_nodes > + > + @field_validator("nodes") > + @classmethod > + def validate_node_names(cls, nodes: list[NodeConfiguration]) -> list= [NodeConfiguration]: > + """Validate that the node names are unique.""" > + nodes_by_name: dict[str, int] =3D {} > + for node_no, node in enumerate(nodes): > + assert node.name not in nodes_by_name, ( > + f"node {node_no} cannot have the same name as node {node= s_by_name[node.name]} " > + f"({node.name})" > + ) > + nodes_by_name[node.name] =3D node_no > + > + return nodes > + > + @model_validator(mode=3D"after") > + def validate_ports(self) -> Self: > + """Validate that the ports are all linked to valid ones.""" > + port_links: dict[tuple[str, str], Literal[False] | tuple[int, in= t]] =3D { > + (node.name, port.pci): False for node in self.nodes for port= in node.ports > + } > + > + for node_no, node in enumerate(self.nodes): > + for port_no, port in enumerate(node.ports): > + peer_port_identifier =3D (port.peer_node, port.peer_pci) > + peer_port =3D port_links.get(peer_port_identifier, None) > + assert peer_port is not None, ( > + "invalid peer port specified for " f"nodes.{node_no}= .ports.{port_no}" > + ) > + assert peer_port is False, ( > + f"the peer port specified for nodes.{node_no}.ports.= {port_no} " > + f"is already linked to nodes.{peer_port[0]}.ports.{p= eer_port[1]}" > + ) > + port_links[peer_port_identifier] =3D (node_no, port_no) > > - return cls(test_runs=3Dtest_runs) > + return self > + > + @model_validator(mode=3D"after") > + def validate_test_runs_with_nodes(self) -> Self: > + """Validate the test runs to nodes associations. > + > + This validator relies on the cached property `test_runs_with_nod= es` to run for the first > + time in this call, therefore triggering the assertions if needed= . > + """ > + if self.test_runs_with_nodes: > + pass > + return self > > > def load_config(config_file_path: Path) -> Configuration: > """Load DTS test run configuration from a file. > > - Load the YAML test run configuration file > - and :download:`the configuration file schema = `, > - validate the test run configuration file, and create a test run conf= iguration object. > + Load the YAML test run configuration file, validate it, and create a= test run configuration > + object. > > The YAML test run configuration file is specified in the :option:`--= config-file` command line > argument or the :envvar:`DTS_CFG_FILE` environment variable. > @@ -671,14 +663,14 @@ def load_config(config_file_path: Path) -> Configur= ation: > > Returns: > The parsed test run configuration. > + > + Raises: > + ConfigurationError: If the supplied configuration file is invali= d. > """ > with open(config_file_path, "r") as f: > config_data =3D yaml.safe_load(f) > > - schema_path =3D os.path.join(Path(__file__).parent.resolve(), "conf_= yaml_schema.json") > - > - with open(schema_path, "r") as f: > - schema =3D json.load(f) > - config =3D warlock.model_factory(schema, name=3D"_Config")(config_da= ta) > - config_obj: Configuration =3D Configuration.from_dict(dict(config)) = # type: ignore[arg-type] > - return config_obj > + try: > + return Configuration.model_validate(config_data) > + except ValidationError as e: > + raise ConfigurationError("failed to load the supplied configurat= ion") from e > diff --git a/dts/framework/config/conf_yaml_schema.json b/dts/framework/c= onfig/conf_yaml_schema.json > deleted file mode 100644 > index cc3e78cef5..0000000000 > --- a/dts/framework/config/conf_yaml_schema.json > +++ /dev/null > @@ -1,459 +0,0 @@ > -{ > - "$schema": "https://json-schema.org/draft-07/schema", > - "title": "DTS Config Schema", > - "definitions": { > - "node_name": { > - "type": "string", > - "description": "A unique identifier for a node" > - }, > - "NIC": { > - "type": "string", > - "enum": [ > - "ALL", > - "ConnectX3_MT4103", > - "ConnectX4_LX_MT4117", > - "ConnectX4_MT4115", > - "ConnectX5_MT4119", > - "ConnectX5_MT4121", > - "I40E_10G-10G_BASE_T_BC", > - "I40E_10G-10G_BASE_T_X722", > - "I40E_10G-SFP_X722", > - "I40E_10G-SFP_XL710", > - "I40E_10G-X722_A0", > - "I40E_1G-1G_BASE_T_X722", > - "I40E_25G-25G_SFP28", > - "I40E_40G-QSFP_A", > - "I40E_40G-QSFP_B", > - "IAVF-ADAPTIVE_VF", > - "IAVF-VF", > - "IAVF_10G-X722_VF", > - "ICE_100G-E810C_QSFP", > - "ICE_25G-E810C_SFP", > - "ICE_25G-E810_XXV_SFP", > - "IGB-I350_VF", > - "IGB_1G-82540EM", > - "IGB_1G-82545EM_COPPER", > - "IGB_1G-82571EB_COPPER", > - "IGB_1G-82574L", > - "IGB_1G-82576", > - "IGB_1G-82576_QUAD_COPPER", > - "IGB_1G-82576_QUAD_COPPER_ET2", > - "IGB_1G-82580_COPPER", > - "IGB_1G-I210_COPPER", > - "IGB_1G-I350_COPPER", > - "IGB_1G-I354_SGMII", > - "IGB_1G-PCH_LPTLP_I218_LM", > - "IGB_1G-PCH_LPTLP_I218_V", > - "IGB_1G-PCH_LPT_I217_LM", > - "IGB_1G-PCH_LPT_I217_V", > - "IGB_2.5G-I354_BACKPLANE_2_5GBPS", > - "IGC-I225_LM", > - "IGC-I226_LM", > - "IXGBE_10G-82599_SFP", > - "IXGBE_10G-82599_SFP_SF_QP", > - "IXGBE_10G-82599_T3_LOM", > - "IXGBE_10G-82599_VF", > - "IXGBE_10G-X540T", > - "IXGBE_10G-X540_VF", > - "IXGBE_10G-X550EM_A_SFP", > - "IXGBE_10G-X550EM_X_10G_T", > - "IXGBE_10G-X550EM_X_SFP", > - "IXGBE_10G-X550EM_X_VF", > - "IXGBE_10G-X550T", > - "IXGBE_10G-X550_VF", > - "brcm_57414", > - "brcm_P2100G", > - "cavium_0011", > - "cavium_a034", > - "cavium_a063", > - "cavium_a064", > - "fastlinq_ql41000", > - "fastlinq_ql41000_vf", > - "fastlinq_ql45000", > - "fastlinq_ql45000_vf", > - "hi1822", > - "virtio" > - ] > - }, > - > - "ARCH": { > - "type": "string", > - "enum": [ > - "x86_64", > - "arm64", > - "ppc64le" > - ] > - }, > - "OS": { > - "type": "string", > - "enum": [ > - "linux" > - ] > - }, > - "cpu": { > - "type": "string", > - "description": "Native should be the default on x86", > - "enum": [ > - "native", > - "armv8a", > - "dpaa2", > - "thunderx", > - "xgene1" > - ] > - }, > - "compiler": { > - "type": "string", > - "enum": [ > - "gcc", > - "clang", > - "icc", > - "mscv" > - ] > - }, > - "build_options": { > - "type": "object", > - "properties": { > - "arch": { > - "type": "string", > - "enum": [ > - "ALL", > - "x86_64", > - "arm64", > - "ppc64le", > - "other" > - ] > - }, > - "os": { > - "$ref": "#/definitions/OS" > - }, > - "cpu": { > - "$ref": "#/definitions/cpu" > - }, > - "compiler": { > - "$ref": "#/definitions/compiler" > - }, > - "compiler_wrapper": { > - "type": "string", > - "description": "This will be added before compiler to the CC v= ariable when building DPDK. Optional." > - } > - }, > - "additionalProperties": false, > - "required": [ > - "arch", > - "os", > - "cpu", > - "compiler" > - ] > - }, > - "dpdk_build": { > - "type": "object", > - "description": "DPDK source and build configuration.", > - "properties": { > - "dpdk_tree": { > - "type": "string", > - "description": "The path to the DPDK source tree directory to = test. Only one of `dpdk_tree` or `tarball` must be provided." > - }, > - "tarball": { > - "type": "string", > - "description": "The path to the DPDK source tarball to test. O= nly one of `dpdk_tree` or `tarball` must be provided." > - }, > - "remote": { > - "type": "boolean", > - "description": "Optional, defaults to false. If it's true, the= `dpdk_tree` or `tarball` is located on the SUT node, instead of the execut= ion host." > - }, > - "precompiled_build_dir": { > - "type": "string", > - "description": "If it's defined, DPDK has been pre-built and t= he build directory is located in a subdirectory of DPDK tree root directory= . Otherwise, will be using a `build_options` to build the DPDK from source.= Either this or `build_options` must be defined, but not both." > - }, > - "build_options": { > - "$ref": "#/definitions/build_options", > - "description": "Either this or `precompiled_build_dir` must be= defined, but not both. DPDK build configuration supported by DTS." > - } > - }, > - "allOf": [ > - { > - "oneOf": [ > - { > - "required": [ > - "dpdk_tree" > - ] > - }, > - { > - "required": [ > - "tarball" > - ] > - } > - ] > - }, > - { > - "oneOf": [ > - { > - "required": [ > - "precompiled_build_dir" > - ] > - }, > - { > - "required": [ > - "build_options" > - ] > - } > - ] > - } > - ], > - "additionalProperties": false > - }, > - "hugepages_2mb": { > - "type": "object", > - "description": "Optional hugepage configuration. If not specified,= hugepages won't be configured and DTS will use system configuration.", > - "properties": { > - "number_of": { > - "type": "integer", > - "description": "The number of hugepages to configure. Hugepage= size will be the system default." > - }, > - "force_first_numa": { > - "type": "boolean", > - "description": "Set to True to force configuring hugepages on = the first NUMA node. Defaults to False." > - } > - }, > - "additionalProperties": false, > - "required": [ > - "number_of" > - ] > - }, > - "mac_address": { > - "type": "string", > - "description": "A MAC address", > - "pattern": "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$" > - }, > - "pci_address": { > - "type": "string", > - "pattern": "^[\\da-fA-F]{4}:[\\da-fA-F]{2}:[\\da-fA-F]{2}.\\d:?\\w= *$" > - }, > - "port_peer_address": { > - "description": "Peer is a TRex port, and IXIA port or a PCI addres= s", > - "oneOf": [ > - { > - "description": "PCI peer port", > - "$ref": "#/definitions/pci_address" > - } > - ] > - }, > - "test_suite": { > - "type": "string", > - "enum": [ > - "hello_world", > - "os_udp", > - "pmd_buffer_scatter", > - "vlan" > - ] > - }, > - "test_target": { > - "type": "object", > - "properties": { > - "suite": { > - "$ref": "#/definitions/test_suite" > - }, > - "cases": { > - "type": "array", > - "description": "If specified, only this subset of test suite's= test cases will be run.", > - "items": { > - "type": "string" > - }, > - "minimum": 1 > - } > - }, > - "required": [ > - "suite" > - ], > - "additionalProperties": false > - } > - }, > - "type": "object", > - "properties": { > - "nodes": { > - "type": "array", > - "items": { > - "type": "object", > - "properties": { > - "name": { > - "type": "string", > - "description": "A unique identifier for this node" > - }, > - "hostname": { > - "type": "string", > - "description": "A hostname from which the node running DTS c= an access this node. This can also be an IP address." > - }, > - "user": { > - "type": "string", > - "description": "The user to access this node with." > - }, > - "password": { > - "type": "string", > - "description": "The password to use on this node. Use only a= s a last resort. SSH keys are STRONGLY preferred." > - }, > - "arch": { > - "$ref": "#/definitions/ARCH" > - }, > - "os": { > - "$ref": "#/definitions/OS" > - }, > - "lcores": { > - "type": "string", > - "pattern": "^(([0-9]+|([0-9]+-[0-9]+))(,([0-9]+|([0-9]+-[0-9= ]+)))*)?$", > - "description": "Optional comma-separated list of logical cor= es to use, e.g.: 1,2,3,4,5,18-22. Defaults to 1. An empty string means use = all lcores." > - }, > - "use_first_core": { > - "type": "boolean", > - "description": "Indicate whether DPDK should use the first p= hysical core. It won't be used by default." > - }, > - "memory_channels": { > - "type": "integer", > - "description": "How many memory channels to use. Optional, d= efaults to 1." > - }, > - "hugepages_2mb": { > - "$ref": "#/definitions/hugepages_2mb" > - }, > - "ports": { > - "type": "array", > - "items": { > - "type": "object", > - "description": "Each port should be described on both side= s of the connection. This makes configuration slightly more verbose but gre= atly simplifies implementation. If there are inconsistencies, then DTS will= not run until that issue is fixed. An example inconsistency would be port = 1, node 1 says it is connected to port 1, node 2, but port 1, node 2 says i= t is connected to port 2, node 1.", > - "properties": { > - "pci": { > - "$ref": "#/definitions/pci_address", > - "description": "The local PCI address of the port" > - }, > - "os_driver_for_dpdk": { > - "type": "string", > - "description": "The driver that the kernel should bind= this device to for DPDK to use it. (ex: vfio-pci)" > - }, > - "os_driver": { > - "type": "string", > - "description": "The driver normally used by this port = (ex: i40e)" > - }, > - "peer_node": { > - "type": "string", > - "description": "The name of the node the peer port is = on" > - }, > - "peer_pci": { > - "$ref": "#/definitions/pci_address", > - "description": "The PCI address of the peer port" > - } > - }, > - "additionalProperties": false, > - "required": [ > - "pci", > - "os_driver_for_dpdk", > - "os_driver", > - "peer_node", > - "peer_pci" > - ] > - }, > - "minimum": 1 > - }, > - "traffic_generator": { > - "oneOf": [ > - { > - "type": "object", > - "description": "Scapy traffic generator. Used for functi= onal testing.", > - "properties": { > - "type": { > - "type": "string", > - "enum": [ > - "SCAPY" > - ] > - } > - } > - } > - ] > - } > - }, > - "additionalProperties": false, > - "required": [ > - "name", > - "hostname", > - "user", > - "arch", > - "os" > - ] > - }, > - "minimum": 1 > - }, > - "test_runs": { > - "type": "array", > - "items": { > - "type": "object", > - "properties": { > - "dpdk_build": { > - "$ref": "#/definitions/dpdk_build" > - }, > - "perf": { > - "type": "boolean", > - "description": "Enable performance testing." > - }, > - "func": { > - "type": "boolean", > - "description": "Enable functional testing." > - }, > - "test_suites": { > - "type": "array", > - "items": { > - "oneOf": [ > - { > - "$ref": "#/definitions/test_suite" > - }, > - { > - "$ref": "#/definitions/test_target" > - } > - ] > - } > - }, > - "skip_smoke_tests": { > - "description": "Optional field that allows you to skip smoke= testing", > - "type": "boolean" > - }, > - "system_under_test_node": { > - "type":"object", > - "properties": { > - "node_name": { > - "$ref": "#/definitions/node_name" > - }, > - "vdevs": { > - "description": "Optional list of names of vdevs to be us= ed in the test run", > - "type": "array", > - "items": { > - "type": "string" > - } > - } > - }, > - "required": [ > - "node_name" > - ] > - }, > - "traffic_generator_node": { > - "$ref": "#/definitions/node_name" > - }, > - "random_seed": { > - "type": "integer", > - "description": "Optional field. Allows you to set a seed for= pseudo-random generation." > - } > - }, > - "additionalProperties": false, > - "required": [ > - "dpdk_build", > - "perf", > - "func", > - "test_suites", > - "system_under_test_node", > - "traffic_generator_node" > - ] > - }, > - "minimum": 1 > - } > - }, > - "required": [ > - "test_runs", > - "nodes" > - ], > - "additionalProperties": false > -} > diff --git a/dts/framework/config/types.py b/dts/framework/config/types.p= y > deleted file mode 100644 > index 02e738a61e..0000000000 > --- a/dts/framework/config/types.py > +++ /dev/null > @@ -1,149 +0,0 @@ > -# SPDX-License-Identifier: BSD-3-Clause > -# Copyright(c) 2023 PANTHEON.tech s.r.o. > - > -"""Configuration dictionary contents specification. > - > -These type definitions serve as documentation of the configuration dicti= onary contents. > - > -The definitions use the built-in :class:`~typing.TypedDict` construct. > -""" > - > -from typing import TypedDict > - > - > -class PortConfigDict(TypedDict): > - """Allowed keys and values.""" > - > - #: > - pci: str > - #: > - os_driver_for_dpdk: str > - #: > - os_driver: str > - #: > - peer_node: str > - #: > - peer_pci: str > - > - > -class TrafficGeneratorConfigDict(TypedDict): > - """Allowed keys and values.""" > - > - #: > - type: str > - > - > -class HugepageConfigurationDict(TypedDict): > - """Allowed keys and values.""" > - > - #: > - number_of: int > - #: > - force_first_numa: bool > - > - > -class NodeConfigDict(TypedDict): > - """Allowed keys and values.""" > - > - #: > - hugepages_2mb: HugepageConfigurationDict > - #: > - name: str > - #: > - hostname: str > - #: > - user: str > - #: > - password: str > - #: > - arch: str > - #: > - os: str > - #: > - lcores: str > - #: > - use_first_core: bool > - #: > - ports: list[PortConfigDict] > - #: > - memory_channels: int > - #: > - traffic_generator: TrafficGeneratorConfigDict > - > - > -class DPDKBuildConfigDict(TypedDict): > - """Allowed keys and values.""" > - > - #: > - arch: str > - #: > - os: str > - #: > - cpu: str > - #: > - compiler: str > - #: > - compiler_wrapper: str > - > - > -class DPDKConfigurationDict(TypedDict): > - """Allowed keys and values.""" > - > - #: > - dpdk_tree: str | None > - #: > - tarball: str | None > - #: > - remote: bool > - #: > - precompiled_build_dir: str | None > - #: > - build_options: DPDKBuildConfigDict > - > - > -class TestSuiteConfigDict(TypedDict): > - """Allowed keys and values.""" > - > - #: > - suite: str > - #: > - cases: list[str] > - > - > -class TestRunSUTConfigDict(TypedDict): > - """Allowed keys and values.""" > - > - #: > - node_name: str > - #: > - vdevs: list[str] > - > - > -class TestRunConfigDict(TypedDict): > - """Allowed keys and values.""" > - > - #: > - dpdk_build: DPDKConfigurationDict > - #: > - perf: bool > - #: > - func: bool > - #: > - skip_smoke_tests: bool > - #: > - test_suites: TestSuiteConfigDict > - #: > - system_under_test_node: TestRunSUTConfigDict > - #: > - traffic_generator_node: str > - #: > - random_seed: int > - > - > -class ConfigurationDict(TypedDict): > - """Allowed keys and values.""" > - > - #: > - nodes: list[NodeConfigDict] > - #: > - test_runs: list[TestRunConfigDict] > diff --git a/dts/framework/runner.py b/dts/framework/runner.py > index 195622c653..c3d9a27a8c 100644 > --- a/dts/framework/runner.py > +++ b/dts/framework/runner.py > @@ -30,7 +30,15 @@ > from framework.testbed_model.sut_node import SutNode > from framework.testbed_model.tg_node import TGNode > > -from .config import Configuration, TestRunConfiguration, TestSuiteConfig= , load_config > +from .config import ( > + Configuration, > + DPDKPrecompiledBuildConfiguration, > + SutNodeConfiguration, > + TestRunConfiguration, > + TestSuiteConfig, > + TGNodeConfiguration, > + load_config, > +) > from .exception import ( > BlockingTestSuiteError, > ConfigurationError, > @@ -133,11 +141,10 @@ def run(self) -> None: > self._result.update_setup(Result.PASS) > > # for all test run sections > - for test_run_config in self._configuration.test_runs: > + for test_run_with_nodes_config in self._configuration.test_r= uns_with_nodes: > + test_run_config, sut_node_config, tg_node_config =3D tes= t_run_with_nodes_config > self._logger.set_stage(DtsStage.test_run_setup) > - self._logger.info( > - f"Running test run with SUT '{test_run_config.system= _under_test_node.name}'." > - ) > + self._logger.info(f"Running test run with SUT '{sut_node= _config.name}'.") > self._init_random_seed(test_run_config) > test_run_result =3D self._result.add_test_run(test_run_c= onfig) > # we don't want to modify the original config, so create= a copy > @@ -145,7 +152,7 @@ def run(self) -> None: > SETTINGS.test_suites if SETTINGS.test_suites else te= st_run_config.test_suites > ) > if not test_run_config.skip_smoke_tests: > - test_run_test_suites[:0] =3D [TestSuiteConfig.from_d= ict("smoke_tests")] > + test_run_test_suites[:0] =3D [TestSuiteConfig(test_s= uite=3D"smoke_tests")] > try: > test_suites_with_cases =3D self._get_test_suites_wit= h_cases( > test_run_test_suites, test_run_config.func, test= _run_config.perf > @@ -161,6 +168,8 @@ def run(self) -> None: > self._connect_nodes_and_run_test_run( > sut_nodes, > tg_nodes, > + sut_node_config, > + tg_node_config, > test_run_config, > test_run_result, > test_suites_with_cases, > @@ -223,10 +232,10 @@ def _get_test_suites_with_cases( > test_suites_with_cases =3D [] > > for test_suite_config in test_suite_configs: > - test_suite_class =3D self._get_test_suite_class(test_suite_c= onfig.test_suite) > + test_suite_class =3D self._get_test_suite_class(test_suite_c= onfig.test_suite_name) > test_cases: list[type[TestCase]] =3D [] > func_test_cases, perf_test_cases =3D test_suite_class.filter= _test_cases( > - test_suite_config.test_cases > + test_suite_config.test_cases_names > ) > if func: > test_cases.extend(func_test_cases) > @@ -305,6 +314,8 @@ def _connect_nodes_and_run_test_run( > self, > sut_nodes: dict[str, SutNode], > tg_nodes: dict[str, TGNode], > + sut_node_config: SutNodeConfiguration, > + tg_node_config: TGNodeConfiguration, > test_run_config: TestRunConfiguration, > test_run_result: TestRunResult, > test_suites_with_cases: Iterable[TestSuiteWithCases], > @@ -319,24 +330,26 @@ def _connect_nodes_and_run_test_run( > Args: > sut_nodes: A dictionary storing connected/to be connected SU= T nodes. > tg_nodes: A dictionary storing connected/to be connected TG = nodes. > + sut_node_config: The test run's SUT node configuration. > + tg_node_config: The test run's TG node configuration. > test_run_config: A test run configuration. > test_run_result: The test run's result. > test_suites_with_cases: The test suites with test cases to r= un. > """ > - sut_node =3D sut_nodes.get(test_run_config.system_under_test_nod= e.name) > - tg_node =3D tg_nodes.get(test_run_config.traffic_generator_node.= name) > + sut_node =3D sut_nodes.get(sut_node_config.name) > + tg_node =3D tg_nodes.get(tg_node_config.name) > > try: > if not sut_node: > - sut_node =3D SutNode(test_run_config.system_under_test_n= ode) > + sut_node =3D SutNode(sut_node_config) > sut_nodes[sut_node.name] =3D sut_node > if not tg_node: > - tg_node =3D TGNode(test_run_config.traffic_generator_nod= e) > + tg_node =3D TGNode(tg_node_config) > tg_nodes[tg_node.name] =3D tg_node > except Exception as e: > - failed_node =3D test_run_config.system_under_test_node.name > + failed_node =3D test_run_config.system_under_test_node.node_= name > if sut_node: > - failed_node =3D test_run_config.traffic_generator_node.n= ame > + failed_node =3D test_run_config.traffic_generator_node > self._logger.exception(f"The Creation of node {failed_node} = failed.") > test_run_result.update_setup(Result.FAIL, e) > > @@ -369,14 +382,22 @@ def _run_test_run( > ConfigurationError: If the DPDK sources or build is not set = up from config or settings. > """ > self._logger.info( > - f"Running test run with SUT '{test_run_config.system_under_t= est_node.name}'." > + f"Running test run with SUT '{test_run_config.system_under_t= est_node.node_name}'." > ) > test_run_result.add_sut_info(sut_node.node_info) > try: > - dpdk_location =3D SETTINGS.dpdk_location or test_run_config.= dpdk_config.dpdk_location > - sut_node.set_up_test_run(test_run_config, dpdk_location) > + dpdk_build_config =3D test_run_config.dpdk_config > + if new_location :=3D SETTINGS.dpdk_location: > + dpdk_build_config =3D dpdk_build_config.model_copy( > + update=3D{"dpdk_location": new_location} > + ) > + if dir :=3D SETTINGS.precompiled_build_dir: > + dpdk_build_config =3D DPDKPrecompiledBuildConfiguration( > + dpdk_location=3Ddpdk_build_config.dpdk_location, pre= compiled_build_dir=3Ddir > + ) > + sut_node.set_up_test_run(test_run_config, dpdk_build_config) > test_run_result.add_dpdk_build_info(sut_node.get_dpdk_build_= info()) > - tg_node.set_up_test_run(test_run_config, dpdk_location) > + tg_node.set_up_test_run(test_run_config, dpdk_build_config) > test_run_result.update_setup(Result.PASS) > except Exception as e: > self._logger.exception("Test run setup failed.") > diff --git a/dts/framework/settings.py b/dts/framework/settings.py > index a452319b90..1253ed86ac 100644 > --- a/dts/framework/settings.py > +++ b/dts/framework/settings.py > @@ -60,9 +60,8 @@ > .. option:: --precompiled-build-dir > .. envvar:: DTS_PRECOMPILED_BUILD_DIR > > - Define the subdirectory under the DPDK tree root directory where the= pre-compiled binaries are > - located. If set, DTS will build DPDK under the `build` directory ins= tead. Can only be used with > - --dpdk-tree or --tarball. > + Define the subdirectory under the DPDK tree root directory or tarbal= l where the pre-compiled > + binaries are located. > > .. option:: --test-suite > .. envvar:: DTS_TEST_SUITES > @@ -95,13 +94,21 @@ > import argparse > import os > import sys > -import tarfile > from argparse import Action, ArgumentDefaultsHelpFormatter, _get_action_= name > from dataclasses import dataclass, field > from pathlib import Path > from typing import Callable > > -from .config import DPDKLocation, TestSuiteConfig > +from pydantic import ValidationError > + > +from .config import ( > + DPDKLocation, > + LocalDPDKTarballLocation, > + LocalDPDKTreeLocation, > + RemoteDPDKTarballLocation, > + RemoteDPDKTreeLocation, > + TestSuiteConfig, > +) > > > @dataclass(slots=3DTrue) > @@ -122,6 +129,8 @@ class Settings: > #: > dpdk_location: DPDKLocation | None =3D None > #: > + precompiled_build_dir: str | None =3D None > + #: > compile_timeout: float =3D 1200 > #: > test_suites: list[TestSuiteConfig] =3D field(default_factory=3Dlist) > @@ -383,13 +392,11 @@ def _get_parser() -> _DTSArgumentParser: > > action =3D dpdk_build.add_argument( > "--precompiled-build-dir", > - help=3D"Define the subdirectory under the DPDK tree root directo= ry where the pre-compiled " > - "binaries are located. If set, DTS will build DPDK under the `bu= ild` directory instead. " > - "Can only be used with --dpdk-tree or --tarball.", > + help=3D"Define the subdirectory under the DPDK tree root directo= ry or tarball where the " > + "pre-compiled binaries are located.", > metavar=3D"DIR_NAME", > ) > _add_env_var_to_action(action) > - _required_with_one_of(parser, action, "dpdk_tarball_path", "dpdk_tre= e_path") > > action =3D parser.add_argument( > "--compile-timeout", > @@ -442,61 +449,61 @@ def _get_parser() -> _DTSArgumentParser: > > > def _process_dpdk_location( > + parser: _DTSArgumentParser, > dpdk_tree: str | None, > tarball: str | None, > remote: bool, > - build_dir: str | None, > -): > +) -> DPDKLocation | None: > """Process and validate DPDK build arguments. > > Ensures that either `dpdk_tree` or `tarball` is provided. Validate e= xistence and format of > `dpdk_tree` or `tarball` on local filesystem, if `remote` is False. = Constructs and returns > - the :class:`DPDKLocation` with the provided parameters if validation= is successful. > + any valid :class:`DPDKLocation` with the provided parameters if vali= dation is successful. > > Args: > - dpdk_tree: The path to the DPDK source tree directory. Only one = of `dpdk_tree` or `tarball` > - must be provided. > - tarball: The path to the DPDK tarball. Only one of `dpdk_tree` o= r `tarball` must be > - provided. > + dpdk_tree: The path to the DPDK source tree directory. > + tarball: The path to the DPDK tarball. > remote: If :data:`True`, `dpdk_tree` or `tarball` is located on = the SUT node, instead of the > execution host. > - build_dir: If it's defined, DPDK has been pre-built and the buil= d directory is located in a > - subdirectory of `dpdk_tree` or `tarball` root directory. > > Returns: > A DPDK location if construction is successful, otherwise None. > - > - Raises: > - argparse.ArgumentTypeError: If `dpdk_tree` or `tarball` not foun= d in local filesystem or > - they aren't in the right format. > """ > - if not (dpdk_tree or tarball): > - return None > - > - if not remote: > - if dpdk_tree: > - if not Path(dpdk_tree).exists(): > - raise argparse.ArgumentTypeError( > - f"DPDK tree '{dpdk_tree}' not found in local filesys= tem." > - ) > - > - if not Path(dpdk_tree).is_dir(): > - raise argparse.ArgumentTypeError(f"DPDK tree '{dpdk_tree= }' must be a directory.") > - > - dpdk_tree =3D os.path.realpath(dpdk_tree) > - > - if tarball: > - if not Path(tarball).exists(): > - raise argparse.ArgumentTypeError( > - f"DPDK tarball '{tarball}' not found in local filesy= stem." > - ) > - > - if not tarfile.is_tarfile(tarball): > - raise argparse.ArgumentTypeError( > - f"DPDK tarball '{tarball}' must be a valid tar archi= ve." > - ) > - > - return DPDKLocation(dpdk_tree=3Ddpdk_tree, tarball=3Dtarball, remote= =3Dremote, build_dir=3Dbuild_dir) > + if dpdk_tree: > + action =3D parser.find_action("dpdk_tree", _is_from_env) > + > + try: > + if remote: > + return RemoteDPDKTreeLocation.model_validate({"dpdk_tree= ": dpdk_tree}) > + else: > + return LocalDPDKTreeLocation.model_validate({"dpdk_tree"= : dpdk_tree}) > + except ValidationError as e: > + print( > + "An error has occurred while validating the DPDK tree su= pplied in the " > + f"{'environment variable' if action else 'arguments'}:", > + file=3Dsys.stderr, > + ) > + print(e, file=3Dsys.stderr) > + sys.exit(1) > + > + if tarball: > + action =3D parser.find_action("tarball", _is_from_env) > + > + try: > + if remote: > + return RemoteDPDKTarballLocation.model_validate({"tarbal= l": tarball}) > + else: > + return LocalDPDKTarballLocation.model_validate({"tarball= ": tarball}) > + except ValidationError as e: > + print( > + "An error has occurred while validating the DPDK tarball= supplied in the " > + f"{'environment variable' if action else 'arguments'}:", > + file=3Dsys.stderr, > + ) > + print(e, file=3Dsys.stderr) > + sys.exit(1) > + > + return None > > > def _process_test_suites( > @@ -512,11 +519,24 @@ def _process_test_suites( > Returns: > A list of test suite configurations to execute. > """ > - if parser.find_action("test_suites", _is_from_env): > + action =3D parser.find_action("test_suites", _is_from_env) > + if action: > # Environment variable in the form of "SUITE1 CASE1 CASE2, SUITE= 2 CASE1, SUITE3, ..." > args =3D [suite_with_cases.split() for suite_with_cases in args[= 0][0].split(",")] > > - return [TestSuiteConfig(test_suite, test_cases) for [test_suite, *te= st_cases] in args] > + try: > + return [ > + TestSuiteConfig(test_suite=3Dtest_suite, test_cases=3Dtest_c= ases) > + for [test_suite, *test_cases] in args > + ] > + except ValidationError as e: > + print( > + "An error has occurred while validating the test suites supp= lied in the " > + f"{'environment variable' if action else 'arguments'}:", > + file=3Dsys.stderr, > + ) > + print(e, file=3Dsys.stderr) > + sys.exit(1) > > > def get_settings() -> Settings: > @@ -536,7 +556,7 @@ def get_settings() -> Settings: > args =3D parser.parse_args() > > args.dpdk_location =3D _process_dpdk_location( > - args.dpdk_tree_path, args.dpdk_tarball_path, args.remote_source,= args.precompiled_build_dir > + parser, args.dpdk_tree_path, args.dpdk_tarball_path, args.remote= _source > ) > args.test_suites =3D _process_test_suites(parser, args.test_suites) > > diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_= model/node.py > index 62867fd80c..6031eaf937 100644 > --- a/dts/framework/testbed_model/node.py > +++ b/dts/framework/testbed_model/node.py > @@ -17,7 +17,12 @@ > from ipaddress import IPv4Interface, IPv6Interface > from typing import Union > > -from framework.config import OS, DPDKLocation, NodeConfiguration, TestRu= nConfiguration > +from framework.config import ( > + OS, > + DPDKBuildConfiguration, > + NodeConfiguration, > + TestRunConfiguration, > +) > from framework.exception import ConfigurationError > from framework.logger import DTSLogger, get_dts_logger > > @@ -89,13 +94,15 @@ def __init__(self, node_config: NodeConfiguration): > self._init_ports() > > def _init_ports(self) -> None: > - self.ports =3D [Port(port_config) for port_config in self.config= .ports] > + self.ports =3D [Port(self.name, port_config) for port_config in = self.config.ports] > self.main_session.update_ports(self.ports) > for port in self.ports: > self.configure_port_state(port) > > def set_up_test_run( > - self, test_run_config: TestRunConfiguration, dpdk_location: DPDK= Location > + self, > + test_run_config: TestRunConfiguration, > + dpdk_build_config: DPDKBuildConfiguration, > ) -> None: > """Test run setup steps. > > @@ -105,7 +112,7 @@ def set_up_test_run( > Args: > test_run_config: A test run configuration according to which > the setup steps will be taken. > - dpdk_location: The target source of the DPDK tree. > + dpdk_build_config: The build configuration of DPDK. > """ > self._setup_hugepages() > > diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/te= stbed_model/os_session.py > index 5f087f40d6..42ab4bb8fd 100644 > --- a/dts/framework/testbed_model/os_session.py > +++ b/dts/framework/testbed_model/os_session.py > @@ -364,7 +364,7 @@ def extract_remote_tarball( > """ > > @abstractmethod > - def is_remote_dir(self, remote_path: str) -> bool: > + def is_remote_dir(self, remote_path: PurePath) -> bool: > """Check if the `remote_path` is a directory. > > Args: > @@ -375,7 +375,7 @@ def is_remote_dir(self, remote_path: str) -> bool: > """ > > @abstractmethod > - def is_remote_tarfile(self, remote_tarball_path: str) -> bool: > + def is_remote_tarfile(self, remote_tarball_path: PurePath) -> bool: > """Check if the `remote_tarball_path` is a tar archive. > > Args: > diff --git a/dts/framework/testbed_model/port.py b/dts/framework/testbed_= model/port.py > index 82c84cf4f8..817405bea4 100644 > --- a/dts/framework/testbed_model/port.py > +++ b/dts/framework/testbed_model/port.py > @@ -54,7 +54,7 @@ class Port: > mac_address: str =3D "" > logical_name: str =3D "" > > - def __init__(self, config: PortConfig): > + def __init__(self, node_name: str, config: PortConfig): > """Initialize the port from `node_name` and `config`. > > Args: > @@ -62,7 +62,7 @@ def __init__(self, config: PortConfig): > config: The test run configuration of the port. > """ > self.identifier =3D PortIdentifier( > - node=3Dconfig.node, > + node=3Dnode_name, > pci=3Dconfig.pci, > ) > self.os_driver =3D config.os_driver > diff --git a/dts/framework/testbed_model/posix_session.py b/dts/framework= /testbed_model/posix_session.py > index 0d3abbc519..6b66f33e22 100644 > --- a/dts/framework/testbed_model/posix_session.py > +++ b/dts/framework/testbed_model/posix_session.py > @@ -201,12 +201,12 @@ def extract_remote_tarball( > if expected_dir: > self.send_command(f"ls {expected_dir}", verify=3DTrue) > > - def is_remote_dir(self, remote_path: str) -> bool: > + def is_remote_dir(self, remote_path: PurePath) -> bool: > """Overrides :meth:`~.os_session.OSSession.is_remote_dir`.""" > result =3D self.send_command(f"test -d {remote_path}") > return not result.return_code > > - def is_remote_tarfile(self, remote_tarball_path: str) -> bool: > + def is_remote_tarfile(self, remote_tarball_path: PurePath) -> bool: > """Overrides :meth:`~.os_session.OSSession.is_remote_tarfile`.""= " > result =3D self.send_command(f"tar -tvf {remote_tarball_path}") > return not result.return_code > @@ -393,4 +393,8 @@ def get_node_info(self) -> NodeInfo: > SETTINGS.timeout, > ).stdout.split("\n") > kernel_version =3D self.send_command("uname -r", SETTINGS.timeou= t).stdout > - return NodeInfo(os_release_info[0].strip(), os_release_info[1].s= trip(), kernel_version) > + return NodeInfo( > + os_name=3Dos_release_info[0].strip(), > + os_version=3Dos_release_info[1].strip(), > + kernel_version=3Dkernel_version, > + ) > diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/test= bed_model/sut_node.py > index a6c42b548c..57337c8e7d 100644 > --- a/dts/framework/testbed_model/sut_node.py > +++ b/dts/framework/testbed_model/sut_node.py > @@ -15,11 +15,17 @@ > import os > import time > from dataclasses import dataclass > -from pathlib import PurePath > +from pathlib import Path, PurePath > > from framework.config import ( > DPDKBuildConfiguration, > - DPDKLocation, > + DPDKBuildOptionsConfiguration, > + DPDKPrecompiledBuildConfiguration, > + DPDKUncompiledBuildConfiguration, > + LocalDPDKTarballLocation, > + LocalDPDKTreeLocation, > + RemoteDPDKTarballLocation, > + RemoteDPDKTreeLocation, > SutNodeConfiguration, > TestRunConfiguration, > ) > @@ -178,7 +184,9 @@ def get_dpdk_build_info(self) -> DPDKBuildInfo: > return DPDKBuildInfo(dpdk_version=3Dself.dpdk_version, compiler_= version=3Dself.compiler_version) > > def set_up_test_run( > - self, test_run_config: TestRunConfiguration, dpdk_location: DPDK= Location > + self, > + test_run_config: TestRunConfiguration, > + dpdk_build_config: DPDKBuildConfiguration, > ) -> None: > """Extend the test run setup with vdev config and DPDK build set= up. > > @@ -188,12 +196,12 @@ def set_up_test_run( > Args: > test_run_config: A test run configuration according to which > the setup steps will be taken. > - dpdk_location: The target source of the DPDK tree. > + dpdk_build_config: The build configuration of DPDK. > """ > - super().set_up_test_run(test_run_config, dpdk_location) > - for vdev in test_run_config.vdevs: > + super().set_up_test_run(test_run_config, dpdk_build_config) > + for vdev in test_run_config.system_under_test_node.vdevs: > self.virtual_devices.append(VirtualDevice(vdev)) > - self._set_up_dpdk(dpdk_location, test_run_config.dpdk_config.dpd= k_build_config) > + self._set_up_dpdk(dpdk_build_config) > > def tear_down_test_run(self) -> None: > """Extend the test run teardown with virtual device teardown and= DPDK teardown.""" > @@ -202,7 +210,8 @@ def tear_down_test_run(self) -> None: > self._tear_down_dpdk() > > def _set_up_dpdk( > - self, dpdk_location: DPDKLocation, dpdk_build_config: DPDKBuildC= onfiguration | None > + self, > + dpdk_build_config: DPDKBuildConfiguration, > ) -> None: > """Set up DPDK the SUT node and bind ports. > > @@ -211,21 +220,26 @@ def _set_up_dpdk( > are bound to those that DPDK needs. > > Args: > - dpdk_location: The location of the DPDK tree. > - dpdk_build_config: A DPDK build configuration to test. If :d= ata:`None`, > - DTS will use pre-built DPDK from a :dataclass:`DPDKLocat= ion`. > + dpdk_build_config: A DPDK build configuration to test. > """ > - self._set_remote_dpdk_tree_path(dpdk_location.dpdk_tree, dpdk_lo= cation.remote) > - if not self._remote_dpdk_tree_path: > - if dpdk_location.dpdk_tree: > - self._copy_dpdk_tree(dpdk_location.dpdk_tree) > - elif dpdk_location.tarball: > - self._prepare_and_extract_dpdk_tarball(dpdk_location.tar= ball, dpdk_location.remote) > - > - self._set_remote_dpdk_build_dir(dpdk_location.build_dir) > - if not self.remote_dpdk_build_dir and dpdk_build_config: > - self._configure_dpdk_build(dpdk_build_config) > - self._build_dpdk() > + match dpdk_build_config.dpdk_location: > + case RemoteDPDKTreeLocation(dpdk_tree=3Ddpdk_tree): > + self._set_remote_dpdk_tree_path(dpdk_tree) > + case LocalDPDKTreeLocation(dpdk_tree=3Ddpdk_tree): > + self._copy_dpdk_tree(dpdk_tree) > + case RemoteDPDKTarballLocation(tarball=3Dtarball): > + self._validate_remote_dpdk_tarball(tarball) > + self._prepare_and_extract_dpdk_tarball(tarball) > + case LocalDPDKTarballLocation(tarball=3Dtarball): > + remote_tarball =3D self._copy_dpdk_tarball_to_remote(tar= ball) > + self._prepare_and_extract_dpdk_tarball(remote_tarball) > + > + match dpdk_build_config: > + case DPDKPrecompiledBuildConfiguration(precompiled_build_dir= =3Dbuild_dir): > + self._set_remote_dpdk_build_dir(build_dir) > + case DPDKUncompiledBuildConfiguration(build_options=3Dbuild_= options): > + self._configure_dpdk_build(build_options) > + self._build_dpdk() > > self.bind_ports_to_driver() > > @@ -238,37 +252,29 @@ def _tear_down_dpdk(self) -> None: > self.compiler_version =3D None > self.bind_ports_to_driver(for_dpdk=3DFalse) > > - def _set_remote_dpdk_tree_path(self, dpdk_tree: str | None, remote: = bool): > + def _set_remote_dpdk_tree_path(self, dpdk_tree: PurePath): > """Set the path to the remote DPDK source tree based on the prov= ided DPDK location. > > - If :data:`dpdk_tree` and :data:`remote` are defined, check exist= ence of :data:`dpdk_tree` > - on SUT node and sets the `_remote_dpdk_tree_path` property. Othe= rwise, sets nothing. > - > Verify DPDK source tree existence on the SUT node, if exists set= s the > `_remote_dpdk_tree_path` property, otherwise sets nothing. > > Args: > dpdk_tree: The path to the DPDK source tree directory. > - remote: Indicates whether the `dpdk_tree` is already on the = SUT node, instead of the > - execution host. > > Raises: > RemoteFileNotFoundError: If the DPDK source tree is expected= to be on the SUT node but > is not found. > """ > - if remote and dpdk_tree: > - if not self.main_session.remote_path_exists(dpdk_tree): > - raise RemoteFileNotFoundError( > - f"Remote DPDK source tree '{dpdk_tree}' not found in= SUT node." > - ) > - if not self.main_session.is_remote_dir(dpdk_tree): > - raise ConfigurationError( > - f"Remote DPDK source tree '{dpdk_tree}' must be a di= rectory." > - ) > - > - self.__remote_dpdk_tree_path =3D PurePath(dpdk_tree) > - > - def _copy_dpdk_tree(self, dpdk_tree_path: str) -> None: > + if not self.main_session.remote_path_exists(dpdk_tree): > + raise RemoteFileNotFoundError( > + f"Remote DPDK source tree '{dpdk_tree}' not found in SUT= node." > + ) > + if not self.main_session.is_remote_dir(dpdk_tree): > + raise ConfigurationError(f"Remote DPDK source tree '{dpdk_tr= ee}' must be a directory.") > + > + self.__remote_dpdk_tree_path =3D dpdk_tree > + > + def _copy_dpdk_tree(self, dpdk_tree_path: Path) -> None: > """Copy the DPDK source tree to the SUT. > > Args: > @@ -288,25 +294,45 @@ def _copy_dpdk_tree(self, dpdk_tree_path: str) -> N= one: > self._remote_tmp_dir, PurePath(dpdk_tree_path).name > ) > > - def _prepare_and_extract_dpdk_tarball(self, dpdk_tarball: str, remot= e: bool) -> None: > - """Ensure the DPDK tarball is available on the SUT node and extr= act it. > + def _validate_remote_dpdk_tarball(self, dpdk_tarball: PurePath) -> N= one: > + """Validate the DPDK tarball on the SUT node. > > - This method ensures that the DPDK source tree tarball is availab= le on the > - SUT node. If the `dpdk_tarball` is local, it is copied to the SU= T node. If the > - `dpdk_tarball` is already on the SUT node, it verifies its exist= ence. > - The `dpdk_tarball` is then extracted on the SUT node. > + Args: > + dpdk_tarball: The path to the DPDK tarball on the SUT node. > > - This method sets the `_remote_dpdk_tree_path` property to the pa= th of the > - extracted DPDK tree on the SUT node. > + Raises: > + RemoteFileNotFoundError: If the `dpdk_tarball` is expected t= o be on the SUT node but is > + not found. > + ConfigurationError: If the `dpdk_tarball` is a valid path bu= t not a valid tar archive. > + """ > + if not self.main_session.remote_path_exists(dpdk_tarball): > + raise RemoteFileNotFoundError(f"Remote DPDK tarball '{dpdk_t= arball}' not found in SUT.") > + if not self.main_session.is_remote_tarfile(dpdk_tarball): > + raise ConfigurationError(f"Remote DPDK tarball '{dpdk_tarbal= l}' must be a tar archive.") > + > + def _copy_dpdk_tarball_to_remote(self, dpdk_tarball: Path) -> PurePa= th: > + """Copy the local DPDK tarball to the SUT node. > > Args: > - dpdk_tarball: The path to the DPDK tarball, either locally o= r on the SUT node. > - remote: Indicates whether the `dpdk_tarball` is already on t= he SUT node, instead of the > - execution host. > + dpdk_tarball: The local path to the DPDK tarball. > > - Raises: > - RemoteFileNotFoundError: If the `dpdk_tarball` is expected t= o be on the SUT node but > - is not found. > + Returns: > + The path of the copied tarball on the SUT node. > + """ > + self._logger.info( > + f"Copying DPDK tarball to SUT: '{dpdk_tarball}' into '{self.= _remote_tmp_dir}'." > + ) > + self.main_session.copy_to(dpdk_tarball, self._remote_tmp_dir) > + return self.main_session.join_remote_path(self._remote_tmp_dir, = dpdk_tarball.name) > + > + def _prepare_and_extract_dpdk_tarball(self, remote_tarball_path: Pur= ePath) -> None: > + """Prepare the remote DPDK tree path and extract the tarball. > + > + This method extracts the remote tarball and sets the `_remote_dp= dk_tree_path` property to > + the path of the extracted DPDK tree on the SUT node. > + > + Args: > + remote_tarball_path: The path to the DPDK tarball on the SUT= node. > """ > > def remove_tarball_suffix(remote_tarball_path: PurePath) -> Pure= Path: > @@ -324,30 +350,9 @@ def remove_tarball_suffix(remote_tarball_path: PureP= ath) -> PurePath: > return PurePath(str(remote_tarball_path).replace(suf= fixes_to_remove, "")) > return remote_tarball_path.with_suffix("") > > - if remote: > - if not self.main_session.remote_path_exists(dpdk_tarball): > - raise RemoteFileNotFoundError( > - f"Remote DPDK tarball '{dpdk_tarball}' not found in = SUT." > - ) > - if not self.main_session.is_remote_tarfile(dpdk_tarball): > - raise ConfigurationError( > - f"Remote DPDK tarball '{dpdk_tarball}' must be a tar= archive." > - ) > - > - remote_tarball_path =3D PurePath(dpdk_tarball) > - else: > - self._logger.info( > - f"Copying DPDK tarball to SUT: '{dpdk_tarball}' into '{s= elf._remote_tmp_dir}'." > - ) > - self.main_session.copy_to(dpdk_tarball, self._remote_tmp_dir= ) > - > - remote_tarball_path =3D self.main_session.join_remote_path( > - self._remote_tmp_dir, PurePath(dpdk_tarball).name > - ) > - > tarball_top_dir =3D self.main_session.get_tarball_top_dir(remote= _tarball_path) > self.__remote_dpdk_tree_path =3D self.main_session.join_remote_p= ath( > - PurePath(remote_tarball_path).parent, > + remote_tarball_path.parent, > tarball_top_dir or remove_tarball_suffix(remote_tarball_path= ), > ) > > @@ -360,33 +365,32 @@ def remove_tarball_suffix(remote_tarball_path: Pure= Path) -> PurePath: > self._remote_dpdk_tree_path, > ) > > - def _set_remote_dpdk_build_dir(self, build_dir: str | None): > + def _set_remote_dpdk_build_dir(self, build_dir: str): > """Set the `remote_dpdk_build_dir` on the SUT. > > - If :data:`build_dir` is defined, check existence on the SUT node= and sets the > + Check existence on the SUT node and sets the > `remote_dpdk_build_dir` property by joining the `_remote_dpdk_tr= ee_path` and `build_dir`. > Otherwise, sets nothing. > > Args: > - build_dir: If it's defined, DPDK has been pre-built and the = build directory is located > + build_dir: DPDK has been pre-built and the build directory i= s located > in a subdirectory of `dpdk_tree` or `tarball` root direc= tory. > > Raises: > RemoteFileNotFoundError: If the `build_dir` is expected but = does not exist on the SUT > node. > """ > - if build_dir: > - remote_dpdk_build_dir =3D self.main_session.join_remote_path= ( > - self._remote_dpdk_tree_path, build_dir > + remote_dpdk_build_dir =3D self.main_session.join_remote_path( > + self._remote_dpdk_tree_path, build_dir > + ) > + if not self.main_session.remote_path_exists(remote_dpdk_build_di= r): > + raise RemoteFileNotFoundError( > + f"Remote DPDK build dir '{remote_dpdk_build_dir}' not fo= und in SUT node." > ) > - if not self.main_session.remote_path_exists(remote_dpdk_buil= d_dir): > - raise RemoteFileNotFoundError( > - f"Remote DPDK build dir '{remote_dpdk_build_dir}' no= t found in SUT node." > - ) > > - self._remote_dpdk_build_dir =3D PurePath(remote_dpdk_build_d= ir) > + self._remote_dpdk_build_dir =3D PurePath(remote_dpdk_build_dir) > > - def _configure_dpdk_build(self, dpdk_build_config: DPDKBuildConfigur= ation) -> None: > + def _configure_dpdk_build(self, dpdk_build_config: DPDKBuildOptionsC= onfiguration) -> None: > """Populate common environment variables and set the DPDK build = related properties. > > This method sets `compiler_version` for additional information a= nd `remote_dpdk_build_dir` > diff --git a/dts/framework/testbed_model/topology.py b/dts/framework/test= bed_model/topology.py > index d38ae36c2a..17b333e76a 100644 > --- a/dts/framework/testbed_model/topology.py > +++ b/dts/framework/testbed_model/topology.py > @@ -99,7 +99,16 @@ def __init__(self, sut_ports: Iterable[Port], tg_ports= : Iterable[Port]): > port_links.append(PortLink(sut_port=3Dsut_port, tg_p= ort=3Dtg_port)) > > self.type =3D TopologyType.get_from_value(len(port_links)) > - dummy_port =3D Port(PortConfig("", "", "", "", "", "")) > + dummy_port =3D Port( > + "", > + PortConfig( > + pci=3D"0000:00:00.0", > + os_driver_for_dpdk=3D"", > + os_driver=3D"", > + peer_node=3D"", > + peer_pci=3D"0000:00:00.0", > + ), > + ) > self.tg_port_egress =3D dummy_port > self.sut_port_ingress =3D dummy_port > self.sut_port_egress =3D dummy_port > diff --git a/dts/framework/testbed_model/traffic_generator/__init__.py b/= dts/framework/testbed_model/traffic_generator/__init__.py > index a319fa5320..945f6bbbbb 100644 > --- a/dts/framework/testbed_model/traffic_generator/__init__.py > +++ b/dts/framework/testbed_model/traffic_generator/__init__.py > @@ -38,6 +38,4 @@ def create_traffic_generator( > case ScapyTrafficGeneratorConfig(): > return ScapyTrafficGenerator(tg_node, traffic_generator_conf= ig, privileged=3DTrue) > case _: > - raise ConfigurationError( > - f"Unknown traffic generator: {traffic_generator_config.t= raffic_generator_type}" > - ) > + raise ConfigurationError(f"Unknown traffic generator: {traff= ic_generator_config.type}") > diff --git a/dts/framework/testbed_model/traffic_generator/traffic_genera= tor.py b/dts/framework/testbed_model/traffic_generator/traffic_generator.py > index 469a12a780..5ac61cd4e1 100644 > --- a/dts/framework/testbed_model/traffic_generator/traffic_generator.py > +++ b/dts/framework/testbed_model/traffic_generator/traffic_generator.py > @@ -45,7 +45,7 @@ def __init__(self, tg_node: Node, config: TrafficGenera= torConfig, **kwargs): > """ > self._config =3D config > self._tg_node =3D tg_node > - self._logger =3D get_dts_logger(f"{self._tg_node.name} {self._co= nfig.traffic_generator_type}") > + self._logger =3D get_dts_logger(f"{self._tg_node.name} {self._co= nfig.type}") > super().__init__(tg_node, **kwargs) > > def send_packet(self, packet: Packet, port: Port) -> None: > diff --git a/dts/framework/utils.py b/dts/framework/utils.py > index 78a39e32c7..e862e3ac66 100644 > --- a/dts/framework/utils.py > +++ b/dts/framework/utils.py > @@ -28,7 +28,7 @@ > > from .exception import InternalError > > -REGEX_FOR_PCI_ADDRESS: str =3D "/[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-= F]{2}.[0-9]{1}/" > +REGEX_FOR_PCI_ADDRESS: str =3D r"[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-= F]{2}.[0-9]{1}" > _REGEX_FOR_COLON_OR_HYPHEN_SEP_MAC: str =3D r"(?:[\da-fA-F]{2}[:-]){5}[\= da-fA-F]{2}" > _REGEX_FOR_DOT_SEP_MAC: str =3D r"(?:[\da-fA-F]{4}.){2}[\da-fA-F]{4}" > REGEX_FOR_MAC_ADDRESS: str =3D rf"{_REGEX_FOR_COLON_OR_HYPHEN_SEP_MAC}|{= _REGEX_FOR_DOT_SEP_MAC}" > diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smo= ke_tests.py > index d7870bd40f..bc3a2a6bf9 100644 > --- a/dts/tests/TestSuite_smoke_tests.py > +++ b/dts/tests/TestSuite_smoke_tests.py > @@ -127,7 +127,7 @@ def test_device_bound_to_driver(self) -> None: > path_to_devbind =3D self.sut_node.path_to_devbind_script > > all_nics_in_dpdk_devbind =3D self.sut_node.main_session.send_com= mand( > - f"{path_to_devbind} --status | awk '{REGEX_FOR_PCI_ADDRESS}'= ", > + f"{path_to_devbind} --status | awk '/{REGEX_FOR_PCI_ADDRESS}= /'", > SETTINGS.timeout, > ).stdout > > -- > 2.43.0 >