* [PATCH v1] dts: add support for no link topology
@ 2026-02-20 19:25 Andrew Bailey
2026-02-24 16:32 ` [PATCH v2] " Andrew Bailey
` (2 more replies)
0 siblings, 3 replies; 12+ messages in thread
From: Andrew Bailey @ 2026-02-20 19:25 UTC (permalink / raw)
To: probb, dev; +Cc: dmarx, luca.vizzarro, Andrew Bailey
Add support for running DTS with no traffic generator node and no ethdev
interfaces. Some applications, like dpdk-test-crypto-perf run without
ethdev interfaces and no traffic generator. In these cases, it is
beneficial to remove the overhead of creating a node and ports that are
not used.
Bugzilla ID: 1870
Signed-off-by: Andrew Bailey <abailey@iol.unh.edu>
---
dts/api/test.py | 8 +++++---
dts/configurations/test_run.example.yaml | 4 ++--
dts/framework/config/__init__.py | 16 ++++++++-------
dts/framework/config/node.py | 2 +-
dts/framework/config/test_run.py | 4 ++--
dts/framework/context.py | 2 +-
dts/framework/remote_session/dpdk.py | 3 ++-
dts/framework/test_run.py | 25 ++++++++++++++----------
dts/framework/testbed_model/topology.py | 17 +++++++++++-----
9 files changed, 49 insertions(+), 32 deletions(-)
diff --git a/dts/api/test.py b/dts/api/test.py
index e17babe0ca..046f1778a5 100644
--- a/dts/api/test.py
+++ b/dts/api/test.py
@@ -109,12 +109,14 @@ def fail(failure_description: str) -> None:
Raises:
TestCaseVerifyError: Always raised to indicate the test case failed.
"""
+ ctx = get_ctx()
get_logger().debug("A test case failed, showing the last 10 commands executed on SUT:")
- for command_res in get_ctx().sut_node.main_session.remote_session.history[-10:]:
+ for command_res in ctx.sut_node.main_session.remote_session.history[-10:]:
get_logger().debug(command_res.command)
get_logger().debug("A test case failed, showing the last 10 commands executed on TG:")
- for command_res in get_ctx().tg_node.main_session.remote_session.history[-10:]:
- get_logger().debug(command_res.command)
+ if ctx.tg_node:
+ for command_res in ctx.tg_node.main_session.remote_session.history[-10:]:
+ get_logger().debug(command_res.command)
raise TestCaseVerifyError(failure_description)
diff --git a/dts/configurations/test_run.example.yaml b/dts/configurations/test_run.example.yaml
index c8035fccf0..f6ca09545e 100644
--- a/dts/configurations/test_run.example.yaml
+++ b/dts/configurations/test_run.example.yaml
@@ -23,7 +23,7 @@ dpdk:
# in a subdirectory of DPDK tree root directory. Otherwise, will be using the `build_options`
# to build the DPDK from source. Either `precompiled_build_dir` or `build_options` can be
# defined, but not both.
-func_traffic_generator:
+func_traffic_generator: # Remove both traffic generators when running a no-link topology
type: SCAPY
# perf_traffic_generator:
# type: TREX
@@ -42,7 +42,7 @@ vdevs: # optional; if removed, vdevs won't be used in the execution
system_under_test_node: "SUT 1"
# Traffic generator node to use for this execution environment
traffic_generator_node: "TG 1"
-port_topology:
+port_topology: # provide empty list for no-link environment
- sut.port-0 <-> tg.port-0 # explicit link. `sut` and `tg` are special identifiers that refer
# to the respective test run's configured nodes.
- port-1 <-> port-1 # implicit link, left side is always SUT, right side is always TG.
diff --git a/dts/framework/config/__init__.py b/dts/framework/config/__init__.py
index d2f0138e4a..59d566aa0b 100644
--- a/dts/framework/config/__init__.py
+++ b/dts/framework/config/__init__.py
@@ -94,9 +94,10 @@ def validate_port_links(self) -> Self:
f"already linked to port {sut_node_port_peer[0]}.{sut_node_port_peer[1]}."
)
- tg_node_port_peer = existing_port_links.get(
- (self.test_run.traffic_generator_node, link.tg_port), None
- )
+ if self.test_run.traffic_generator_node:
+ tg_node_port_peer = existing_port_links.get(
+ (self.test_run.traffic_generator_node, link.tg_port), None
+ )
assert (
tg_node_port_peer is not None
), f"Invalid TG node port specified for link port_topology.{link_idx}."
@@ -122,11 +123,12 @@ def validate_test_run_against_nodes(self) -> Self:
), f"The system_under_test_node {sut_node_name} is not a valid node name."
tg_node_name = self.test_run.traffic_generator_node
- tg_node = next((n for n in self.nodes if n.name == tg_node_name), None)
+ if self.test_run.port_topology:
+ tg_node = next((n for n in self.nodes if n.name == tg_node_name), None)
- assert (
- tg_node is not None
- ), f"The traffic_generator_name {tg_node_name} is not a valid node name."
+ assert (
+ tg_node is not None
+ ), f"The traffic_generator_name {tg_node_name} is not a valid node name."
return self
diff --git a/dts/framework/config/node.py b/dts/framework/config/node.py
index 438a1bdc8f..d91c0835e4 100644
--- a/dts/framework/config/node.py
+++ b/dts/framework/config/node.py
@@ -69,7 +69,7 @@ class NodeConfiguration(FrozenModel):
#: An optional hugepage configuration.
hugepages: HugepageConfiguration | None = Field(None, alias="hugepages_2mb")
#: The ports that can be used in testing.
- ports: list[PortConfig] = Field(min_length=1)
+ ports: list[PortConfig] = Field(default=[], min_length=0)
@model_validator(mode="after")
def verify_unique_port_names(self) -> Self:
diff --git a/dts/framework/config/test_run.py b/dts/framework/config/test_run.py
index 6c292a3675..07440c5250 100644
--- a/dts/framework/config/test_run.py
+++ b/dts/framework/config/test_run.py
@@ -490,11 +490,11 @@ class TestRunConfiguration(FrozenModel):
#: The SUT node name to use in this test run.
system_under_test_node: str
#: The TG node name to use in this test run.
- traffic_generator_node: str
+ traffic_generator_node: str | None = Field(default=None)
#: The seed to use for pseudo-random generation.
random_seed: int | None = None
#: The port links between the specified nodes to use.
- port_topology: list[PortLinkConfig] = Field(max_length=2)
+ port_topology: list[PortLinkConfig] = Field(default=[], max_length=2)
fields_from_settings = model_validator(mode="before")(
load_fields_from_settings("test_suites", "random_seed")
diff --git a/dts/framework/context.py b/dts/framework/context.py
index 8f1021dc96..371473f61c 100644
--- a/dts/framework/context.py
+++ b/dts/framework/context.py
@@ -72,7 +72,7 @@ class Context:
"""Runtime context."""
sut_node: Node
- tg_node: Node
+ tg_node: Node | None
topology: Topology
dpdk_build: "DPDKBuildEnvironment"
dpdk: "DPDKRuntimeEnvironment"
diff --git a/dts/framework/remote_session/dpdk.py b/dts/framework/remote_session/dpdk.py
index c3575cfcaf..d63114b0e2 100644
--- a/dts/framework/remote_session/dpdk.py
+++ b/dts/framework/remote_session/dpdk.py
@@ -13,6 +13,7 @@
from pathlib import Path, PurePath
from typing import ClassVar, Final
+from api.capabilities import LinkTopology
from framework.config.test_run import (
DPDKBuildConfiguration,
DPDKBuildOptionsConfiguration,
@@ -263,7 +264,7 @@ def _build_dpdk(self) -> None:
ctx = get_ctx()
# If the SUT is an ice driver device, make sure to build with 16B descriptors.
if (
- ctx.topology.sut_port_ingress
+ ctx.topology.type is not LinkTopology.NO_LINK and not ctx.topology.sut_port_ingress
and ctx.topology.sut_port_ingress.config.os_driver == "ice"
):
meson_args = MesonArgs(
diff --git a/dts/framework/test_run.py b/dts/framework/test_run.py
index ff0a12c9ce..536d0f33fb 100644
--- a/dts/framework/test_run.py
+++ b/dts/framework/test_run.py
@@ -189,25 +189,28 @@ def __init__(
self.config = config
self.logger = get_dts_logger()
+ tg_node = None
sut_node = next(n for n in nodes if n.name == config.system_under_test_node)
- tg_node = next(n for n in nodes if n.name == config.traffic_generator_node)
-
- topology = Topology.from_port_links(
- PortLink(sut_node.ports_by_name[link.sut_port], tg_node.ports_by_name[link.tg_port])
- for link in self.config.port_topology
- )
+ if config.traffic_generator_node:
+ tg_node = next(n for n in nodes if n.name == config.traffic_generator_node)
+ topology = Topology.from_port_links(
+ PortLink(sut_node.ports_by_name[link.sut_port], tg_node.ports_by_name[link.tg_port])
+ for link in self.config.port_topology
+ )
+ else:
+ topology = Topology.from_port_links(iter([]))
dpdk_build_env = DPDKBuildEnvironment(config.dpdk.build, sut_node)
dpdk_runtime_env = DPDKRuntimeEnvironment(config.dpdk, sut_node, dpdk_build_env)
func_traffic_generator = (
create_traffic_generator(config.func_traffic_generator, tg_node)
- if config.func and config.func_traffic_generator
+ if config.func and config.func_traffic_generator and tg_node
else None
)
perf_traffic_generator = (
create_traffic_generator(config.perf_traffic_generator, tg_node)
- if config.perf and config.perf_traffic_generator
+ if config.perf and config.perf_traffic_generator and tg_node
else None
)
@@ -343,7 +346,8 @@ def next(self) -> State | None:
test_run.remaining_tests = deque(test_run.selected_tests)
test_run.ctx.sut_node.setup()
- test_run.ctx.tg_node.setup()
+ if test_run.ctx.tg_node:
+ test_run.ctx.tg_node.setup()
test_run.ctx.dpdk.setup()
test_run.ctx.topology.setup()
@@ -450,7 +454,8 @@ def next(self) -> State | None:
self.test_run.ctx.perf_tg.teardown()
self.test_run.ctx.topology.teardown()
self.test_run.ctx.dpdk.teardown()
- self.test_run.ctx.tg_node.teardown()
+ if self.test_run.ctx.tg_node:
+ self.test_run.ctx.tg_node.teardown()
self.test_run.ctx.sut_node.teardown()
return None
diff --git a/dts/framework/testbed_model/topology.py b/dts/framework/testbed_model/topology.py
index 13b4b58a74..186ef6bad6 100644
--- a/dts/framework/testbed_model/topology.py
+++ b/dts/framework/testbed_model/topology.py
@@ -70,6 +70,8 @@ def from_port_links(cls, port_links: Iterator[PortLink]) -> Self:
ConfigurationError: If an unsupported link topology is supplied.
"""
type = LinkTopology.NO_LINK
+ sut_ports = []
+ tg_ports = []
if port_link := next(port_links, None):
type = LinkTopology.ONE_LINK
@@ -100,16 +102,18 @@ def node_and_ports_from_id(self, node_identifier: NodeIdentifier) -> tuple[Node,
case "sut":
return ctx.sut_node, self.sut_ports
case "tg":
- return ctx.tg_node, self.tg_ports
- case _:
- msg = f"Invalid node `{node_identifier}` given."
- raise InternalError(msg)
+ if self.type is not LinkTopology.NO_LINK:
+ return ctx.tg_node, self.tg_ports
+ msg = f"Invalid node `{node_identifier}` given."
+ raise InternalError(msg)
def setup(self) -> None:
"""Setup topology ports.
Binds all the ports to the right kernel driver to retrieve MAC addresses and logical names.
"""
+ if self.type is LinkTopology.NO_LINK:
+ return
self._prepare_devbind_script()
self._setup_ports("sut")
self._setup_ports("tg")
@@ -119,6 +123,8 @@ def teardown(self) -> None:
Restores all the ports to their original drivers before the test run.
"""
+ if self.type is LinkTopology.NO_LINK:
+ return
self._restore_ports_original_drivers("sut")
self._restore_ports_original_drivers("tg")
@@ -264,7 +270,8 @@ def prepare_node(node: Node) -> None:
node.main_session.devbind_script_path = devbind_script_path
ctx = get_ctx()
- prepare_node(ctx.tg_node)
+ if self.type is not LinkTopology.NO_LINK:
+ prepare_node(ctx.tg_node)
prepare_node(ctx.sut_node)
@property
--
2.50.1
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v2] dts: add support for no link topology
2026-02-20 19:25 [PATCH v1] dts: add support for no link topology Andrew Bailey
@ 2026-02-24 16:32 ` Andrew Bailey
2026-03-09 15:26 ` Andrew Bailey
` (3 more replies)
2026-06-09 13:11 ` [PATCH v3] " Andrew Bailey
2026-06-17 19:32 ` [PATCH v4] " Andrew Bailey
2 siblings, 4 replies; 12+ messages in thread
From: Andrew Bailey @ 2026-02-24 16:32 UTC (permalink / raw)
To: probb; +Cc: luca.vizzarro, dev, dmarx, Andrew Bailey
Add support for running DTS with no traffic generator node and no ethdev
interfaces. Some applications, like dpdk-test-crypto-perf run without
ethdev interfaces and no traffic generator. In these cases, it is
beneficial to remove the overhead of creating a node and ports that are
not used.
Bugzilla ID: 1870
Signed-off-by: Andrew Bailey <abailey@iol.unh.edu>
---
dts/api/test.py | 8 +++++---
dts/configurations/test_run.example.yaml | 4 ++--
dts/framework/config/__init__.py | 16 ++++++++-------
dts/framework/config/node.py | 2 +-
dts/framework/config/test_run.py | 4 ++--
dts/framework/context.py | 2 +-
dts/framework/remote_session/dpdk.py | 4 +++-
dts/framework/test_run.py | 25 ++++++++++++++----------
dts/framework/testbed_model/topology.py | 17 +++++++++++-----
9 files changed, 50 insertions(+), 32 deletions(-)
diff --git a/dts/api/test.py b/dts/api/test.py
index e17babe0ca..046f1778a5 100644
--- a/dts/api/test.py
+++ b/dts/api/test.py
@@ -109,12 +109,14 @@ def fail(failure_description: str) -> None:
Raises:
TestCaseVerifyError: Always raised to indicate the test case failed.
"""
+ ctx = get_ctx()
get_logger().debug("A test case failed, showing the last 10 commands executed on SUT:")
- for command_res in get_ctx().sut_node.main_session.remote_session.history[-10:]:
+ for command_res in ctx.sut_node.main_session.remote_session.history[-10:]:
get_logger().debug(command_res.command)
get_logger().debug("A test case failed, showing the last 10 commands executed on TG:")
- for command_res in get_ctx().tg_node.main_session.remote_session.history[-10:]:
- get_logger().debug(command_res.command)
+ if ctx.tg_node:
+ for command_res in ctx.tg_node.main_session.remote_session.history[-10:]:
+ get_logger().debug(command_res.command)
raise TestCaseVerifyError(failure_description)
diff --git a/dts/configurations/test_run.example.yaml b/dts/configurations/test_run.example.yaml
index c8035fccf0..f6ca09545e 100644
--- a/dts/configurations/test_run.example.yaml
+++ b/dts/configurations/test_run.example.yaml
@@ -23,7 +23,7 @@ dpdk:
# in a subdirectory of DPDK tree root directory. Otherwise, will be using the `build_options`
# to build the DPDK from source. Either `precompiled_build_dir` or `build_options` can be
# defined, but not both.
-func_traffic_generator:
+func_traffic_generator: # Remove both traffic generators when running a no-link topology
type: SCAPY
# perf_traffic_generator:
# type: TREX
@@ -42,7 +42,7 @@ vdevs: # optional; if removed, vdevs won't be used in the execution
system_under_test_node: "SUT 1"
# Traffic generator node to use for this execution environment
traffic_generator_node: "TG 1"
-port_topology:
+port_topology: # provide empty list for no-link environment
- sut.port-0 <-> tg.port-0 # explicit link. `sut` and `tg` are special identifiers that refer
# to the respective test run's configured nodes.
- port-1 <-> port-1 # implicit link, left side is always SUT, right side is always TG.
diff --git a/dts/framework/config/__init__.py b/dts/framework/config/__init__.py
index d2f0138e4a..59d566aa0b 100644
--- a/dts/framework/config/__init__.py
+++ b/dts/framework/config/__init__.py
@@ -94,9 +94,10 @@ def validate_port_links(self) -> Self:
f"already linked to port {sut_node_port_peer[0]}.{sut_node_port_peer[1]}."
)
- tg_node_port_peer = existing_port_links.get(
- (self.test_run.traffic_generator_node, link.tg_port), None
- )
+ if self.test_run.traffic_generator_node:
+ tg_node_port_peer = existing_port_links.get(
+ (self.test_run.traffic_generator_node, link.tg_port), None
+ )
assert (
tg_node_port_peer is not None
), f"Invalid TG node port specified for link port_topology.{link_idx}."
@@ -122,11 +123,12 @@ def validate_test_run_against_nodes(self) -> Self:
), f"The system_under_test_node {sut_node_name} is not a valid node name."
tg_node_name = self.test_run.traffic_generator_node
- tg_node = next((n for n in self.nodes if n.name == tg_node_name), None)
+ if self.test_run.port_topology:
+ tg_node = next((n for n in self.nodes if n.name == tg_node_name), None)
- assert (
- tg_node is not None
- ), f"The traffic_generator_name {tg_node_name} is not a valid node name."
+ assert (
+ tg_node is not None
+ ), f"The traffic_generator_name {tg_node_name} is not a valid node name."
return self
diff --git a/dts/framework/config/node.py b/dts/framework/config/node.py
index 438a1bdc8f..d91c0835e4 100644
--- a/dts/framework/config/node.py
+++ b/dts/framework/config/node.py
@@ -69,7 +69,7 @@ class NodeConfiguration(FrozenModel):
#: An optional hugepage configuration.
hugepages: HugepageConfiguration | None = Field(None, alias="hugepages_2mb")
#: The ports that can be used in testing.
- ports: list[PortConfig] = Field(min_length=1)
+ ports: list[PortConfig] = Field(default=[], min_length=0)
@model_validator(mode="after")
def verify_unique_port_names(self) -> Self:
diff --git a/dts/framework/config/test_run.py b/dts/framework/config/test_run.py
index 6c292a3675..07440c5250 100644
--- a/dts/framework/config/test_run.py
+++ b/dts/framework/config/test_run.py
@@ -490,11 +490,11 @@ class TestRunConfiguration(FrozenModel):
#: The SUT node name to use in this test run.
system_under_test_node: str
#: The TG node name to use in this test run.
- traffic_generator_node: str
+ traffic_generator_node: str | None = Field(default=None)
#: The seed to use for pseudo-random generation.
random_seed: int | None = None
#: The port links between the specified nodes to use.
- port_topology: list[PortLinkConfig] = Field(max_length=2)
+ port_topology: list[PortLinkConfig] = Field(default=[], max_length=2)
fields_from_settings = model_validator(mode="before")(
load_fields_from_settings("test_suites", "random_seed")
diff --git a/dts/framework/context.py b/dts/framework/context.py
index 8f1021dc96..371473f61c 100644
--- a/dts/framework/context.py
+++ b/dts/framework/context.py
@@ -72,7 +72,7 @@ class Context:
"""Runtime context."""
sut_node: Node
- tg_node: Node
+ tg_node: Node | None
topology: Topology
dpdk_build: "DPDKBuildEnvironment"
dpdk: "DPDKRuntimeEnvironment"
diff --git a/dts/framework/remote_session/dpdk.py b/dts/framework/remote_session/dpdk.py
index c3575cfcaf..eedb71c471 100644
--- a/dts/framework/remote_session/dpdk.py
+++ b/dts/framework/remote_session/dpdk.py
@@ -13,6 +13,7 @@
from pathlib import Path, PurePath
from typing import ClassVar, Final
+from api.capabilities import LinkTopology
from framework.config.test_run import (
DPDKBuildConfiguration,
DPDKBuildOptionsConfiguration,
@@ -263,7 +264,8 @@ def _build_dpdk(self) -> None:
ctx = get_ctx()
# If the SUT is an ice driver device, make sure to build with 16B descriptors.
if (
- ctx.topology.sut_port_ingress
+ ctx.topology.type is not LinkTopology.NO_LINK
+ and not ctx.topology.sut_port_ingress
and ctx.topology.sut_port_ingress.config.os_driver == "ice"
):
meson_args = MesonArgs(
diff --git a/dts/framework/test_run.py b/dts/framework/test_run.py
index ff0a12c9ce..536d0f33fb 100644
--- a/dts/framework/test_run.py
+++ b/dts/framework/test_run.py
@@ -189,25 +189,28 @@ def __init__(
self.config = config
self.logger = get_dts_logger()
+ tg_node = None
sut_node = next(n for n in nodes if n.name == config.system_under_test_node)
- tg_node = next(n for n in nodes if n.name == config.traffic_generator_node)
-
- topology = Topology.from_port_links(
- PortLink(sut_node.ports_by_name[link.sut_port], tg_node.ports_by_name[link.tg_port])
- for link in self.config.port_topology
- )
+ if config.traffic_generator_node:
+ tg_node = next(n for n in nodes if n.name == config.traffic_generator_node)
+ topology = Topology.from_port_links(
+ PortLink(sut_node.ports_by_name[link.sut_port], tg_node.ports_by_name[link.tg_port])
+ for link in self.config.port_topology
+ )
+ else:
+ topology = Topology.from_port_links(iter([]))
dpdk_build_env = DPDKBuildEnvironment(config.dpdk.build, sut_node)
dpdk_runtime_env = DPDKRuntimeEnvironment(config.dpdk, sut_node, dpdk_build_env)
func_traffic_generator = (
create_traffic_generator(config.func_traffic_generator, tg_node)
- if config.func and config.func_traffic_generator
+ if config.func and config.func_traffic_generator and tg_node
else None
)
perf_traffic_generator = (
create_traffic_generator(config.perf_traffic_generator, tg_node)
- if config.perf and config.perf_traffic_generator
+ if config.perf and config.perf_traffic_generator and tg_node
else None
)
@@ -343,7 +346,8 @@ def next(self) -> State | None:
test_run.remaining_tests = deque(test_run.selected_tests)
test_run.ctx.sut_node.setup()
- test_run.ctx.tg_node.setup()
+ if test_run.ctx.tg_node:
+ test_run.ctx.tg_node.setup()
test_run.ctx.dpdk.setup()
test_run.ctx.topology.setup()
@@ -450,7 +454,8 @@ def next(self) -> State | None:
self.test_run.ctx.perf_tg.teardown()
self.test_run.ctx.topology.teardown()
self.test_run.ctx.dpdk.teardown()
- self.test_run.ctx.tg_node.teardown()
+ if self.test_run.ctx.tg_node:
+ self.test_run.ctx.tg_node.teardown()
self.test_run.ctx.sut_node.teardown()
return None
diff --git a/dts/framework/testbed_model/topology.py b/dts/framework/testbed_model/topology.py
index 13b4b58a74..e1abc2f0da 100644
--- a/dts/framework/testbed_model/topology.py
+++ b/dts/framework/testbed_model/topology.py
@@ -70,6 +70,8 @@ def from_port_links(cls, port_links: Iterator[PortLink]) -> Self:
ConfigurationError: If an unsupported link topology is supplied.
"""
type = LinkTopology.NO_LINK
+ sut_ports = []
+ tg_ports = []
if port_link := next(port_links, None):
type = LinkTopology.ONE_LINK
@@ -100,16 +102,18 @@ def node_and_ports_from_id(self, node_identifier: NodeIdentifier) -> tuple[Node,
case "sut":
return ctx.sut_node, self.sut_ports
case "tg":
- return ctx.tg_node, self.tg_ports
- case _:
- msg = f"Invalid node `{node_identifier}` given."
- raise InternalError(msg)
+ if ctx.tg_node:
+ return ctx.tg_node, self.tg_ports
+ msg = f"Invalid node `{node_identifier}` given."
+ raise InternalError(msg)
def setup(self) -> None:
"""Setup topology ports.
Binds all the ports to the right kernel driver to retrieve MAC addresses and logical names.
"""
+ if self.type is LinkTopology.NO_LINK:
+ return
self._prepare_devbind_script()
self._setup_ports("sut")
self._setup_ports("tg")
@@ -119,6 +123,8 @@ def teardown(self) -> None:
Restores all the ports to their original drivers before the test run.
"""
+ if self.type is LinkTopology.NO_LINK:
+ return
self._restore_ports_original_drivers("sut")
self._restore_ports_original_drivers("tg")
@@ -264,7 +270,8 @@ def prepare_node(node: Node) -> None:
node.main_session.devbind_script_path = devbind_script_path
ctx = get_ctx()
- prepare_node(ctx.tg_node)
+ if ctx.tg_node:
+ prepare_node(ctx.tg_node)
prepare_node(ctx.sut_node)
@property
--
2.50.1
^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [PATCH v2] dts: add support for no link topology
2026-02-24 16:32 ` [PATCH v2] " Andrew Bailey
@ 2026-03-09 15:26 ` Andrew Bailey
2026-03-09 15:48 ` Andrew Bailey
` (2 subsequent siblings)
3 siblings, 0 replies; 12+ messages in thread
From: Andrew Bailey @ 2026-03-09 15:26 UTC (permalink / raw)
To: probb; +Cc: luca.vizzarro, dev, dmarx
[-- Attachment #1: Type: text/plain, Size: 40 bytes --]
Recheck-request: iol-unit-arm64-testing
[-- Attachment #2: Type: text/html, Size: 82 bytes --]
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v2] dts: add support for no link topology
2026-02-24 16:32 ` [PATCH v2] " Andrew Bailey
2026-03-09 15:26 ` Andrew Bailey
@ 2026-03-09 15:48 ` Andrew Bailey
2026-03-09 16:16 ` Patrick Robb
2026-05-30 20:37 ` [v2] " Patrick Robb
3 siblings, 0 replies; 12+ messages in thread
From: Andrew Bailey @ 2026-03-09 15:48 UTC (permalink / raw)
To: probb; +Cc: luca.vizzarro, dev, dmarx
[-- Attachment #1: Type: text/plain, Size: 43 bytes --]
Recheck-request: ci/iol-unit-arm64-testing
[-- Attachment #2: Type: text/html, Size: 64 bytes --]
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v2] dts: add support for no link topology
2026-02-24 16:32 ` [PATCH v2] " Andrew Bailey
2026-03-09 15:26 ` Andrew Bailey
2026-03-09 15:48 ` Andrew Bailey
@ 2026-03-09 16:16 ` Patrick Robb
2026-04-10 14:36 ` Andrew Bailey
2026-05-30 20:37 ` [v2] " Patrick Robb
3 siblings, 1 reply; 12+ messages in thread
From: Patrick Robb @ 2026-03-09 16:16 UTC (permalink / raw)
To: Andrew Bailey; +Cc: luca.vizzarro, dev, dmarx
Recheck-request: iol-unit-arm64-testing
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v2] dts: add support for no link topology
2026-03-09 16:16 ` Patrick Robb
@ 2026-04-10 14:36 ` Andrew Bailey
0 siblings, 0 replies; 12+ messages in thread
From: Andrew Bailey @ 2026-04-10 14:36 UTC (permalink / raw)
To: Patrick Robb; +Cc: luca.vizzarro, dev, dmarx
[-- Attachment #1: Type: text/plain, Size: 92 bytes --]
This is an update on the status of this patch.
This patch is awaiting review to be merged.
[-- Attachment #2: Type: text/html, Size: 130 bytes --]
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [v2] dts: add support for no link topology
2026-02-24 16:32 ` [PATCH v2] " Andrew Bailey
` (2 preceding siblings ...)
2026-03-09 16:16 ` Patrick Robb
@ 2026-05-30 20:37 ` Patrick Robb
2026-06-01 17:16 ` Andrew Bailey
3 siblings, 1 reply; 12+ messages in thread
From: Patrick Robb @ 2026-05-30 20:37 UTC (permalink / raw)
To: abailey; +Cc: dev, dmarx, luca.vizzarro, probb, Patrick Robb
from pathlib import Path, PurePath
from typing import ClassVar, Final
+from api.capabilities import LinkTopology
from framework.config.test_run import (
DPDKBuildConfiguration,
DPDKBuildOptionsConfiguration,
@@ -263,7 +264,8 @@ def _build_dpdk(self) -> None:
ctx = get_ctx()
# If the SUT is an ice driver device, make sure to build with 16B descriptors.
if (
- ctx.topology.sut_port_ingress
+ ctx.topology.type is not LinkTopology.NO_LINK
+ and not ctx.topology.sut_port_ingress
and ctx.topology.sut_port_ingress.config.os_driver == "ice"
):
I don't understand the change above. Surely we do want to set the custom meson args for ICE DUTs when there is a sut_port_ingress, not when there isn't a sut_port_ingress, right?
Also, does the topology type affect whether we want to use these custom args, if the SUT driver is ice? I guess in a weird case we could have an ethdev test using an ice DUT which does not need a TG?
Reviewed-by: Patrick Robb <patrickrobb1997@gmail.com>
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [v2] dts: add support for no link topology
2026-05-30 20:37 ` [v2] " Patrick Robb
@ 2026-06-01 17:16 ` Andrew Bailey
2026-06-08 22:12 ` Patrick Robb
0 siblings, 1 reply; 12+ messages in thread
From: Andrew Bailey @ 2026-06-01 17:16 UTC (permalink / raw)
To: Patrick Robb; +Cc: dev, dmarx, luca.vizzarro, probb
[-- Attachment #1: Type: text/plain, Size: 822 bytes --]
Hi Patrick, Thanks for the review
> I don't understand the change above. Surely we do want to set the custom
> meson args for ICE DUTs when there is a sut_port_ingress, not when there
> isn't a sut_port_ingress, right?
>
> Yes, this was something I overlooked and will be updated in the next patch.
> Also, does the topology type affect whether we want to use these custom
> args, if the SUT driver is ice? I guess in a weird case we could have an
> ethdev test using an ice DUT which does not need a TG?
>
> My understanding is that if the link topology is no link, there will be no
ingress and the ports on the NIC would not be used for traffic. I do not
believe this argument will still be required if no traffic is being
handled. Please let me know if you disagree with this approach or if I am
missing something.
[-- Attachment #2: Type: text/html, Size: 1219 bytes --]
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [v2] dts: add support for no link topology
2026-06-01 17:16 ` Andrew Bailey
@ 2026-06-08 22:12 ` Patrick Robb
0 siblings, 0 replies; 12+ messages in thread
From: Patrick Robb @ 2026-06-08 22:12 UTC (permalink / raw)
To: Andrew Bailey; +Cc: dev, dmarx, luca.vizzarro, probb
[-- Attachment #1: Type: text/plain, Size: 1154 bytes --]
On Mon, Jun 1, 2026 at 1:16 PM Andrew Bailey <abailey@iol.unh.edu> wrote:
>
> Also, does the topology type affect whether we want to use these custom
>> args, if the SUT driver is ice? I guess in a weird case we could have an
>> ethdev test using an ice DUT which does not need a TG?
>>
>> My understanding is that if the link topology is no link, there will be
> no ingress and the ports on the NIC would not be used for traffic. I do not
> believe this argument will still be required if no traffic is being
> handled. Please let me know if you disagree with this approach or if I am
> missing something.
>
So, yes it probably does not matter which way we decide on this given that
no traffic is going to be transmitted. But, I suppose we might as well
maintain the same logic (compile this way with this DPDK PMD) as with
single link and dual link topologies, unless we have any reason to do
otherwise? But again, if we did compile without the argument in a no_link
topology, I don't think it would matter. So, in this case, whatever your
preference is between these two (as long as you provide a reason), I will
approve it.
[-- Attachment #2: Type: text/html, Size: 1693 bytes --]
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v3] dts: add support for no link topology
2026-02-20 19:25 [PATCH v1] dts: add support for no link topology Andrew Bailey
2026-02-24 16:32 ` [PATCH v2] " Andrew Bailey
@ 2026-06-09 13:11 ` Andrew Bailey
2026-06-17 19:32 ` [PATCH v4] " Andrew Bailey
2 siblings, 0 replies; 12+ messages in thread
From: Andrew Bailey @ 2026-06-09 13:11 UTC (permalink / raw)
To: luca.vizzarro
Cc: patrickrobb1997, dev, ahassick, knimoji, lylavoie, Andrew Bailey
Add support for running DTS with no traffic generator node and no ethdev
interfaces. Some applications, like dpdk-test-crypto-perf run without
ethdev interfaces and no traffic generator. In these cases, it is
beneficial to remove the overhead of creating a node and ports that are
not used. The specified build option for ice devices is removed since
the query to sut port ingress causes python to crash when there are no
ports. Notably, since this is only the case in which there are no ports,
traffic will not be sent and therefore the build argument is not
required. For these reasons it is skipped when running a no-link
topology.
Signed-off-by: Andrew Bailey <abailey@iol.unh.edu>
---
dts/api/test.py | 8 +++++---
dts/configurations/test_run.example.yaml | 2 +-
dts/framework/config/__init__.py | 16 ++++++++-------
dts/framework/config/test_run.py | 4 ++--
dts/framework/context.py | 2 +-
dts/framework/remote_session/dpdk.py | 4 +++-
dts/framework/test_run.py | 25 ++++++++++++++----------
dts/framework/testbed_model/topology.py | 17 +++++++++++-----
8 files changed, 48 insertions(+), 30 deletions(-)
diff --git a/dts/api/test.py b/dts/api/test.py
index e17babe0ca..046f1778a5 100644
--- a/dts/api/test.py
+++ b/dts/api/test.py
@@ -109,12 +109,14 @@ def fail(failure_description: str) -> None:
Raises:
TestCaseVerifyError: Always raised to indicate the test case failed.
"""
+ ctx = get_ctx()
get_logger().debug("A test case failed, showing the last 10 commands executed on SUT:")
- for command_res in get_ctx().sut_node.main_session.remote_session.history[-10:]:
+ for command_res in ctx.sut_node.main_session.remote_session.history[-10:]:
get_logger().debug(command_res.command)
get_logger().debug("A test case failed, showing the last 10 commands executed on TG:")
- for command_res in get_ctx().tg_node.main_session.remote_session.history[-10:]:
- get_logger().debug(command_res.command)
+ if ctx.tg_node:
+ for command_res in ctx.tg_node.main_session.remote_session.history[-10:]:
+ get_logger().debug(command_res.command)
raise TestCaseVerifyError(failure_description)
diff --git a/dts/configurations/test_run.example.yaml b/dts/configurations/test_run.example.yaml
index ee641f5dce..af5f72d305 100644
--- a/dts/configurations/test_run.example.yaml
+++ b/dts/configurations/test_run.example.yaml
@@ -59,7 +59,7 @@ test_suites: # see `Optional Fields`; the following test suites will be run in t
system_under_test_node: "SUT 1"
# Traffic generator node to use for this execution environment
traffic_generator_node: "TG 1"
-port_topology:
+port_topology: # provide empty list for no-link environment
- sut.port-0 <-> tg.port-0 # explicit link. `sut` and `tg` are special identifiers that refer
# to the respective test run's configured nodes.
- port-1 <-> port-1 # implicit link, left side is always SUT, right side is always TG.
diff --git a/dts/framework/config/__init__.py b/dts/framework/config/__init__.py
index d2f0138e4a..59d566aa0b 100644
--- a/dts/framework/config/__init__.py
+++ b/dts/framework/config/__init__.py
@@ -94,9 +94,10 @@ def validate_port_links(self) -> Self:
f"already linked to port {sut_node_port_peer[0]}.{sut_node_port_peer[1]}."
)
- tg_node_port_peer = existing_port_links.get(
- (self.test_run.traffic_generator_node, link.tg_port), None
- )
+ if self.test_run.traffic_generator_node:
+ tg_node_port_peer = existing_port_links.get(
+ (self.test_run.traffic_generator_node, link.tg_port), None
+ )
assert (
tg_node_port_peer is not None
), f"Invalid TG node port specified for link port_topology.{link_idx}."
@@ -122,11 +123,12 @@ def validate_test_run_against_nodes(self) -> Self:
), f"The system_under_test_node {sut_node_name} is not a valid node name."
tg_node_name = self.test_run.traffic_generator_node
- tg_node = next((n for n in self.nodes if n.name == tg_node_name), None)
+ if self.test_run.port_topology:
+ tg_node = next((n for n in self.nodes if n.name == tg_node_name), None)
- assert (
- tg_node is not None
- ), f"The traffic_generator_name {tg_node_name} is not a valid node name."
+ assert (
+ tg_node is not None
+ ), f"The traffic_generator_name {tg_node_name} is not a valid node name."
return self
diff --git a/dts/framework/config/test_run.py b/dts/framework/config/test_run.py
index 76e24d1785..f9143dfc4e 100644
--- a/dts/framework/config/test_run.py
+++ b/dts/framework/config/test_run.py
@@ -492,11 +492,11 @@ class TestRunConfiguration(FrozenModel):
#: The SUT node name to use in this test run.
system_under_test_node: str
#: The TG node name to use in this test run.
- traffic_generator_node: str
+ traffic_generator_node: str | None = Field(default=None)
#: The seed to use for pseudo-random generation.
random_seed: int | None = None
#: The port links between the specified nodes to use.
- port_topology: list[PortLinkConfig] = Field(max_length=2)
+ port_topology: list[PortLinkConfig] = Field(default=[], max_length=2)
fields_from_settings = model_validator(mode="before")(
load_fields_from_settings("test_suites", "random_seed")
diff --git a/dts/framework/context.py b/dts/framework/context.py
index 8f1021dc96..371473f61c 100644
--- a/dts/framework/context.py
+++ b/dts/framework/context.py
@@ -72,7 +72,7 @@ class Context:
"""Runtime context."""
sut_node: Node
- tg_node: Node
+ tg_node: Node | None
topology: Topology
dpdk_build: "DPDKBuildEnvironment"
dpdk: "DPDKRuntimeEnvironment"
diff --git a/dts/framework/remote_session/dpdk.py b/dts/framework/remote_session/dpdk.py
index c3575cfcaf..e43e1f2123 100644
--- a/dts/framework/remote_session/dpdk.py
+++ b/dts/framework/remote_session/dpdk.py
@@ -13,6 +13,7 @@
from pathlib import Path, PurePath
from typing import ClassVar, Final
+from api.capabilities import LinkTopology
from framework.config.test_run import (
DPDKBuildConfiguration,
DPDKBuildOptionsConfiguration,
@@ -263,7 +264,8 @@ def _build_dpdk(self) -> None:
ctx = get_ctx()
# If the SUT is an ice driver device, make sure to build with 16B descriptors.
if (
- ctx.topology.sut_port_ingress
+ ctx.topology.type is not LinkTopology.NO_LINK
+ and ctx.topology.sut_port_ingress
and ctx.topology.sut_port_ingress.config.os_driver == "ice"
):
meson_args = MesonArgs(
diff --git a/dts/framework/test_run.py b/dts/framework/test_run.py
index 94dc6023a7..93fc5ee93f 100644
--- a/dts/framework/test_run.py
+++ b/dts/framework/test_run.py
@@ -189,25 +189,28 @@ def __init__(
self.config = config
self.logger = get_dts_logger()
+ tg_node = None
sut_node = next(n for n in nodes if n.name == config.system_under_test_node)
- tg_node = next(n for n in nodes if n.name == config.traffic_generator_node)
-
- topology = Topology.from_port_links(
- PortLink(sut_node.ports_by_name[link.sut_port], tg_node.ports_by_name[link.tg_port])
- for link in self.config.port_topology
- )
+ if config.traffic_generator_node:
+ tg_node = next(n for n in nodes if n.name == config.traffic_generator_node)
+ topology = Topology.from_port_links(
+ PortLink(sut_node.ports_by_name[link.sut_port], tg_node.ports_by_name[link.tg_port])
+ for link in self.config.port_topology
+ )
+ else:
+ topology = Topology.from_port_links(iter([]))
dpdk_build_env = DPDKBuildEnvironment(config.dpdk.build, sut_node)
dpdk_runtime_env = DPDKRuntimeEnvironment(config.dpdk, sut_node, dpdk_build_env)
func_traffic_generator = (
create_traffic_generator(config.func_traffic_generator, tg_node)
- if config.func and config.func_traffic_generator
+ if config.func and config.func_traffic_generator and tg_node
else None
)
perf_traffic_generator = (
create_traffic_generator(config.perf_traffic_generator, tg_node)
- if config.perf and config.perf_traffic_generator
+ if config.perf and config.perf_traffic_generator and tg_node
else None
)
@@ -343,7 +346,8 @@ def next(self) -> State | None:
test_run.remaining_tests = deque(test_run.selected_tests)
test_run.ctx.sut_node.setup()
- test_run.ctx.tg_node.setup()
+ if test_run.ctx.tg_node:
+ test_run.ctx.tg_node.setup()
test_run.ctx.dpdk.setup()
test_run.ctx.topology.setup()
@@ -455,7 +459,8 @@ def next(self) -> State | None:
self.test_run.ctx.perf_tg.teardown()
self.test_run.ctx.topology.teardown()
self.test_run.ctx.dpdk.teardown()
- self.test_run.ctx.tg_node.teardown()
+ if self.test_run.ctx.tg_node:
+ self.test_run.ctx.tg_node.teardown()
self.test_run.ctx.sut_node.teardown()
return None
diff --git a/dts/framework/testbed_model/topology.py b/dts/framework/testbed_model/topology.py
index 34862c4d2e..01333da716 100644
--- a/dts/framework/testbed_model/topology.py
+++ b/dts/framework/testbed_model/topology.py
@@ -73,6 +73,8 @@ def from_port_links(cls, port_links: Iterator[PortLink]) -> Self:
ConfigurationError: If an unsupported link topology is supplied.
"""
type = LinkTopology.NO_LINK
+ sut_ports = []
+ tg_ports = []
if port_link := next(port_links, None):
type = LinkTopology.ONE_LINK
@@ -103,10 +105,10 @@ def node_and_ports_from_id(self, node_identifier: NodeIdentifier) -> tuple[Node,
case "sut":
return ctx.sut_node, self.sut_ports
case "tg":
- return ctx.tg_node, self.tg_ports
- case _:
- msg = f"Invalid node `{node_identifier}` given."
- raise InternalError(msg)
+ if ctx.tg_node:
+ return ctx.tg_node, self.tg_ports
+ msg = f"Invalid node `{node_identifier}` given."
+ raise InternalError(msg)
def get_crypto_vfs(self, num_vfs: int) -> list[Port]:
"""Retrieve virtual functions from active crypto vfs.
@@ -139,6 +141,8 @@ def setup(self) -> None:
Binds all the ports to the right kernel driver to retrieve MAC addresses and logical names.
"""
+ if self.type is LinkTopology.NO_LINK:
+ return
self._prepare_devbind_script()
self._setup_ports("sut")
self._setup_ports("tg")
@@ -148,6 +152,8 @@ def teardown(self) -> None:
Restores all the ports to their original drivers before the test run.
"""
+ if self.type is LinkTopology.NO_LINK:
+ return
self._restore_ports_original_drivers("sut")
self._restore_ports_original_drivers("tg")
@@ -338,7 +344,8 @@ def prepare_node(node: Node) -> None:
node.main_session.devbind_script_path = devbind_script_path
ctx = get_ctx()
- prepare_node(ctx.tg_node)
+ if ctx.tg_node:
+ prepare_node(ctx.tg_node)
prepare_node(ctx.sut_node)
@property
--
2.54.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v4] dts: add support for no link topology
2026-02-20 19:25 [PATCH v1] dts: add support for no link topology Andrew Bailey
2026-02-24 16:32 ` [PATCH v2] " Andrew Bailey
2026-06-09 13:11 ` [PATCH v3] " Andrew Bailey
@ 2026-06-17 19:32 ` Andrew Bailey
2026-07-03 7:46 ` Luca Vizzarro
2 siblings, 1 reply; 12+ messages in thread
From: Andrew Bailey @ 2026-06-17 19:32 UTC (permalink / raw)
To: patrickrobb1997, luca.vizzarro
Cc: dev, knimoji, lylavoie, ahassick, Andrew Bailey
Add support for running DTS with no traffic generator node and no ethdev
interfaces. Some applications, like dpdk-test-crypto-perf run without
ethdev interfaces and no traffic generator. In these cases, it is
beneficial to remove the overhead of creating a node and ports that are
not used. The specified build option for ice devices is removed since
the query to sut port ingress causes python to crash when there are no
ports. Notably, since this is only the case in which there are no ports,
traffic will not be sent and therefore the build argument is not
required. For these reasons it is skipped when running a no-link
topology.
Signed-off-by: Andrew Bailey <abailey@iol.unh.edu>
---
v4:
* Made it possible to run no link topology even if a TG node is
present in the config.
* Updated the test run example config file to properly document how no
link topology is run.
dts/api/test.py | 11 ++-
dts/configurations/test_run.example.yaml | 7 +-
dts/framework/config/__init__.py | 36 ++++-----
dts/framework/config/test_run.py | 4 +-
dts/framework/context.py | 2 +-
dts/framework/remote_session/dpdk.py | 4 +-
dts/framework/runner.py | 6 +-
dts/framework/test_run.py | 95 ++++++++++++++----------
dts/framework/testbed_model/topology.py | 17 ++++-
9 files changed, 109 insertions(+), 73 deletions(-)
diff --git a/dts/api/test.py b/dts/api/test.py
index e17babe0ca..7947c407d2 100644
--- a/dts/api/test.py
+++ b/dts/api/test.py
@@ -10,6 +10,7 @@
from datetime import datetime
from api.artifact import Artifact
+from api.capabilities import LinkTopology
from framework.context import get_ctx
from framework.exception import InternalError, SkippedTestException, TestCaseVerifyError
from framework.logger import DTSLogger
@@ -109,12 +110,14 @@ def fail(failure_description: str) -> None:
Raises:
TestCaseVerifyError: Always raised to indicate the test case failed.
"""
+ ctx = get_ctx()
get_logger().debug("A test case failed, showing the last 10 commands executed on SUT:")
- for command_res in get_ctx().sut_node.main_session.remote_session.history[-10:]:
- get_logger().debug(command_res.command)
- get_logger().debug("A test case failed, showing the last 10 commands executed on TG:")
- for command_res in get_ctx().tg_node.main_session.remote_session.history[-10:]:
+ for command_res in ctx.sut_node.main_session.remote_session.history[-10:]:
get_logger().debug(command_res.command)
+ if ctx.topology.type is not LinkTopology.NO_LINK and ctx.tg_node is not None:
+ get_logger().debug("A test case failed, showing the last 10 commands executed on TG:")
+ for command_res in ctx.tg_node.main_session.remote_session.history[-10:]:
+ get_logger().debug(command_res.command)
raise TestCaseVerifyError(failure_description)
diff --git a/dts/configurations/test_run.example.yaml b/dts/configurations/test_run.example.yaml
index ee641f5dce..127aa51152 100644
--- a/dts/configurations/test_run.example.yaml
+++ b/dts/configurations/test_run.example.yaml
@@ -25,6 +25,9 @@
# By removing the `test_suites` field, this test run will run every test suite available.
# `vdevs`:
# Uncomment to add a specific virtual device to run on the SUT node.
+# `port_topology`:
+# By providing an empty list, DTS will run in no link topology mode and will not allocate a TG
+# node.
# Define the test run environment
dpdk:
@@ -58,8 +61,8 @@ test_suites: # see `Optional Fields`; the following test suites will be run in t
# The machine running the DPDK test executable
system_under_test_node: "SUT 1"
# Traffic generator node to use for this execution environment
-traffic_generator_node: "TG 1"
-port_topology:
+traffic_generator_node: "TG 1" # see
+port_topology: # see `Optional Fields`
- sut.port-0 <-> tg.port-0 # explicit link. `sut` and `tg` are special identifiers that refer
# to the respective test run's configured nodes.
- port-1 <-> port-1 # implicit link, left side is always SUT, right side is always TG.
diff --git a/dts/framework/config/__init__.py b/dts/framework/config/__init__.py
index d2f0138e4a..abcd9b525f 100644
--- a/dts/framework/config/__init__.py
+++ b/dts/framework/config/__init__.py
@@ -94,17 +94,19 @@ def validate_port_links(self) -> Self:
f"already linked to port {sut_node_port_peer[0]}.{sut_node_port_peer[1]}."
)
- tg_node_port_peer = existing_port_links.get(
- (self.test_run.traffic_generator_node, link.tg_port), None
- )
- assert (
- tg_node_port_peer is not None
- ), f"Invalid TG node port specified for link port_topology.{link_idx}."
-
- assert tg_node_port_peer is False or sut_node_port_peer == link.left, (
- f"The TG node port for link port_topology.{link_idx} is "
- f"already linked to port {tg_node_port_peer[0]}.{tg_node_port_peer[1]}."
- )
+ if self.test_run.port_topology != []:
+ assert self.test_run.traffic_generator_node is not None, "No TG node specified."
+ tg_node_port_peer = existing_port_links.get(
+ (self.test_run.traffic_generator_node, link.tg_port), None
+ )
+ assert (
+ tg_node_port_peer is not None
+ ), f"Invalid TG node port specified for link port_topology.{link_idx}."
+
+ assert tg_node_port_peer is False or sut_node_port_peer == link.left, (
+ f"The TG node port for link port_topology.{link_idx} is "
+ f"already linked to port {tg_node_port_peer[0]}.{tg_node_port_peer[1]}."
+ )
existing_port_links[link.left] = link.right
existing_port_links[link.right] = link.left
@@ -121,13 +123,13 @@ def validate_test_run_against_nodes(self) -> Self:
sut_node is not None
), f"The system_under_test_node {sut_node_name} is not a valid node name."
- tg_node_name = self.test_run.traffic_generator_node
- tg_node = next((n for n in self.nodes if n.name == tg_node_name), None)
-
- assert (
- tg_node is not None
- ), f"The traffic_generator_name {tg_node_name} is not a valid node name."
+ if self.test_run.port_topology != []:
+ tg_node_name = self.test_run.traffic_generator_node
+ tg_node = next((n for n in self.nodes if n.name == tg_node_name), None)
+ assert (
+ tg_node is not None
+ ), f"The traffic_generator_name {tg_node_name} is not a valid node name."
return self
diff --git a/dts/framework/config/test_run.py b/dts/framework/config/test_run.py
index 76e24d1785..f9143dfc4e 100644
--- a/dts/framework/config/test_run.py
+++ b/dts/framework/config/test_run.py
@@ -492,11 +492,11 @@ class TestRunConfiguration(FrozenModel):
#: The SUT node name to use in this test run.
system_under_test_node: str
#: The TG node name to use in this test run.
- traffic_generator_node: str
+ traffic_generator_node: str | None = Field(default=None)
#: The seed to use for pseudo-random generation.
random_seed: int | None = None
#: The port links between the specified nodes to use.
- port_topology: list[PortLinkConfig] = Field(max_length=2)
+ port_topology: list[PortLinkConfig] = Field(default=[], max_length=2)
fields_from_settings = model_validator(mode="before")(
load_fields_from_settings("test_suites", "random_seed")
diff --git a/dts/framework/context.py b/dts/framework/context.py
index 8f1021dc96..371473f61c 100644
--- a/dts/framework/context.py
+++ b/dts/framework/context.py
@@ -72,7 +72,7 @@ class Context:
"""Runtime context."""
sut_node: Node
- tg_node: Node
+ tg_node: Node | None
topology: Topology
dpdk_build: "DPDKBuildEnvironment"
dpdk: "DPDKRuntimeEnvironment"
diff --git a/dts/framework/remote_session/dpdk.py b/dts/framework/remote_session/dpdk.py
index c3575cfcaf..e43e1f2123 100644
--- a/dts/framework/remote_session/dpdk.py
+++ b/dts/framework/remote_session/dpdk.py
@@ -13,6 +13,7 @@
from pathlib import Path, PurePath
from typing import ClassVar, Final
+from api.capabilities import LinkTopology
from framework.config.test_run import (
DPDKBuildConfiguration,
DPDKBuildOptionsConfiguration,
@@ -263,7 +264,8 @@ def _build_dpdk(self) -> None:
ctx = get_ctx()
# If the SUT is an ice driver device, make sure to build with 16B descriptors.
if (
- ctx.topology.sut_port_ingress
+ ctx.topology.type is not LinkTopology.NO_LINK
+ and ctx.topology.sut_port_ingress
and ctx.topology.sut_port_ingress.config.os_driver == "ice"
):
meson_args = MesonArgs(
diff --git a/dts/framework/runner.py b/dts/framework/runner.py
index 6ea4749ff4..fa4f06844e 100644
--- a/dts/framework/runner.py
+++ b/dts/framework/runner.py
@@ -61,7 +61,11 @@ def run(self) -> None:
self._check_dts_python_version()
for node_config in self._configuration.nodes:
- nodes.append(Node(node_config))
+ if self._configuration.test_run.port_topology == []:
+ if node_config.name == self._configuration.test_run.system_under_test_node:
+ nodes.append(Node(node_config))
+ else:
+ nodes.append(Node(node_config))
test_run = TestRun(
self._configuration.test_run,
diff --git a/dts/framework/test_run.py b/dts/framework/test_run.py
index 94dc6023a7..9b973532da 100644
--- a/dts/framework/test_run.py
+++ b/dts/framework/test_run.py
@@ -106,6 +106,7 @@
from types import MethodType
from typing import ClassVar, Protocol, Union
+from api.capabilities import LinkTopology
from framework.config.test_run import TestRunConfiguration
from framework.context import Context, init_ctx
from framework.exception import InternalError, SkippedTestException, TestCaseVerifyError
@@ -190,24 +191,33 @@ def __init__(
self.logger = get_dts_logger()
sut_node = next(n for n in nodes if n.name == config.system_under_test_node)
- tg_node = next(n for n in nodes if n.name == config.traffic_generator_node)
-
- topology = Topology.from_port_links(
- PortLink(sut_node.ports_by_name[link.sut_port], tg_node.ports_by_name[link.tg_port])
- for link in self.config.port_topology
- )
+ if config.port_topology != []:
+ tg_node = next(n for n in nodes if n.name == config.traffic_generator_node)
+ topology = Topology.from_port_links(
+ PortLink(sut_node.ports_by_name[link.sut_port], tg_node.ports_by_name[link.tg_port])
+ for link in self.config.port_topology
+ )
+ else:
+ tg_node = None
+ topology = Topology.from_port_links(iter([]))
dpdk_build_env = DPDKBuildEnvironment(config.dpdk.build, sut_node)
dpdk_runtime_env = DPDKRuntimeEnvironment(config.dpdk, sut_node, dpdk_build_env)
func_traffic_generator = (
create_traffic_generator(config.func_traffic_generator, tg_node)
- if config.func and config.func_traffic_generator
+ if config.func
+ and config.func_traffic_generator
+ and topology.type is not LinkTopology.NO_LINK
+ and tg_node is not None
else None
)
perf_traffic_generator = (
create_traffic_generator(config.perf_traffic_generator, tg_node)
- if config.perf and config.perf_traffic_generator
+ if config.perf
+ and config.perf_traffic_generator
+ and topology.type is not LinkTopology.NO_LINK
+ and tg_node is not None
else None
)
@@ -336,39 +346,40 @@ def description(self) -> str:
def next(self) -> State | None:
"""Process state and return the next one."""
test_run = self.test_run
- init_ctx(test_run.ctx)
+ ctx = test_run.ctx
+ init_ctx(ctx)
- self.logger.info(f"Running on SUT node '{test_run.ctx.sut_node.name}'.")
+ self.logger.info(f"Running on SUT node '{ctx.sut_node.name}'.")
test_run.init_random_seed()
test_run.remaining_tests = deque(test_run.selected_tests)
- test_run.ctx.sut_node.setup()
- test_run.ctx.tg_node.setup()
- test_run.ctx.dpdk.setup()
- test_run.ctx.topology.setup()
+ ctx.sut_node.setup()
+ if ctx.topology.type is not LinkTopology.NO_LINK and ctx.tg_node is not None:
+ ctx.tg_node.setup()
+ ctx.dpdk.setup()
+ ctx.topology.setup()
if test_run.config.use_virtual_functions:
- test_run.ctx.topology.instantiate_vf_ports()
- if test_run.ctx.sut_node.cryptodevs and test_run.config.crypto:
- test_run.ctx.topology.instantiate_crypto_ports()
- test_run.ctx.topology.bind_cryptodevs("dpdk")
+ ctx.topology.instantiate_vf_ports()
+ if ctx.sut_node.cryptodevs and test_run.config.crypto:
+ ctx.topology.instantiate_crypto_ports()
+ ctx.topology.bind_cryptodevs("dpdk")
- test_run.ctx.topology.configure_ports("sut", "dpdk")
- if test_run.ctx.func_tg:
- test_run.ctx.func_tg.setup(test_run.ctx.topology)
- if test_run.ctx.perf_tg:
- test_run.ctx.perf_tg.setup(test_run.ctx.topology)
+ ctx.topology.configure_ports("sut", "dpdk")
+ if ctx.func_tg and ctx.topology.type is not LinkTopology.NO_LINK:
+ ctx.func_tg.setup(ctx.topology)
+ if ctx.perf_tg and ctx.topology.type is not LinkTopology.NO_LINK:
+ ctx.perf_tg.setup(ctx.topology)
self.result.ports = [
- port.to_dict()
- for port in test_run.ctx.topology.sut_ports + test_run.ctx.topology.tg_ports
+ port.to_dict() for port in ctx.topology.sut_ports + ctx.topology.tg_ports
]
- self.result.sut_session_info = test_run.ctx.sut_node.node_info
- self.result.dpdk_build_info = test_run.ctx.dpdk_build.get_dpdk_build_info()
+ self.result.sut_session_info = ctx.sut_node.node_info
+ self.result.dpdk_build_info = ctx.dpdk_build.get_dpdk_build_info()
self.logger.debug(f"Found capabilities to check: {test_run.required_capabilities}")
test_run.supported_capabilities = get_supported_capabilities(
- test_run.ctx.sut_node, test_run.ctx.topology, test_run.required_capabilities
+ ctx.sut_node, ctx.topology, test_run.required_capabilities
)
return TestRunExecution(test_run, self.result)
@@ -443,20 +454,22 @@ def description(self) -> str:
def next(self) -> State | None:
"""Next state."""
+ ctx = self.test_run.ctx
if self.test_run.config.use_virtual_functions:
- self.test_run.ctx.topology.delete_vf_ports()
- if self.test_run.ctx.sut_node.cryptodevs:
- self.test_run.ctx.topology.delete_crypto_vf_ports()
-
- self.test_run.ctx.shell_pool.terminate_current_pool()
- if self.test_run.ctx.func_tg and self.test_run.ctx.func_tg.is_setup:
- self.test_run.ctx.func_tg.teardown()
- if self.test_run.ctx.perf_tg and self.test_run.ctx.perf_tg.is_setup:
- self.test_run.ctx.perf_tg.teardown()
- self.test_run.ctx.topology.teardown()
- self.test_run.ctx.dpdk.teardown()
- self.test_run.ctx.tg_node.teardown()
- self.test_run.ctx.sut_node.teardown()
+ ctx.topology.delete_vf_ports()
+ if ctx.sut_node.cryptodevs:
+ ctx.topology.delete_crypto_vf_ports()
+
+ ctx.shell_pool.terminate_current_pool()
+ if ctx.func_tg is not None and ctx.func_tg.is_setup:
+ ctx.func_tg.teardown()
+ if ctx.perf_tg is not None and ctx.perf_tg.is_setup:
+ ctx.perf_tg.teardown()
+ ctx.topology.teardown()
+ ctx.dpdk.teardown()
+ if ctx.topology.type is not LinkTopology.NO_LINK and ctx.tg_node is not None:
+ ctx.tg_node.teardown()
+ ctx.sut_node.teardown()
return None
def on_error(self, ex: BaseException) -> State | None:
diff --git a/dts/framework/testbed_model/topology.py b/dts/framework/testbed_model/topology.py
index 34862c4d2e..1db444fc01 100644
--- a/dts/framework/testbed_model/topology.py
+++ b/dts/framework/testbed_model/topology.py
@@ -73,6 +73,8 @@ def from_port_links(cls, port_links: Iterator[PortLink]) -> Self:
ConfigurationError: If an unsupported link topology is supplied.
"""
type = LinkTopology.NO_LINK
+ sut_ports = []
+ tg_ports = []
if port_link := next(port_links, None):
type = LinkTopology.ONE_LINK
@@ -103,10 +105,12 @@ def node_and_ports_from_id(self, node_identifier: NodeIdentifier) -> tuple[Node,
case "sut":
return ctx.sut_node, self.sut_ports
case "tg":
- return ctx.tg_node, self.tg_ports
- case _:
- msg = f"Invalid node `{node_identifier}` given."
+ if ctx.tg_node is not None:
+ return ctx.tg_node, self.tg_ports
+ msg = "node tg does not exist with current topology."
raise InternalError(msg)
+ msg = f"Invalid node `{node_identifier}` given."
+ raise InternalError(msg)
def get_crypto_vfs(self, num_vfs: int) -> list[Port]:
"""Retrieve virtual functions from active crypto vfs.
@@ -139,6 +143,8 @@ def setup(self) -> None:
Binds all the ports to the right kernel driver to retrieve MAC addresses and logical names.
"""
+ if self.type is LinkTopology.NO_LINK:
+ return
self._prepare_devbind_script()
self._setup_ports("sut")
self._setup_ports("tg")
@@ -148,6 +154,8 @@ def teardown(self) -> None:
Restores all the ports to their original drivers before the test run.
"""
+ if self.type is LinkTopology.NO_LINK:
+ return
self._restore_ports_original_drivers("sut")
self._restore_ports_original_drivers("tg")
@@ -338,7 +346,8 @@ def prepare_node(node: Node) -> None:
node.main_session.devbind_script_path = devbind_script_path
ctx = get_ctx()
- prepare_node(ctx.tg_node)
+ if ctx.tg_node:
+ prepare_node(ctx.tg_node)
prepare_node(ctx.sut_node)
@property
--
2.54.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [PATCH v4] dts: add support for no link topology
2026-06-17 19:32 ` [PATCH v4] " Andrew Bailey
@ 2026-07-03 7:46 ` Luca Vizzarro
0 siblings, 0 replies; 12+ messages in thread
From: Luca Vizzarro @ 2026-07-03 7:46 UTC (permalink / raw)
To: Andrew Bailey, patrickrobb1997; +Cc: dev, knimoji, lylavoie, ahassick
Hi Andrew,
looks good to me! Thanks,
Reviewed-by: Luca Vizzarro <luca.vizzarro@arm.com>
^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-07-03 7:47 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-02-20 19:25 [PATCH v1] dts: add support for no link topology Andrew Bailey
2026-02-24 16:32 ` [PATCH v2] " Andrew Bailey
2026-03-09 15:26 ` Andrew Bailey
2026-03-09 15:48 ` Andrew Bailey
2026-03-09 16:16 ` Patrick Robb
2026-04-10 14:36 ` Andrew Bailey
2026-05-30 20:37 ` [v2] " Patrick Robb
2026-06-01 17:16 ` Andrew Bailey
2026-06-08 22:12 ` Patrick Robb
2026-06-09 13:11 ` [PATCH v3] " Andrew Bailey
2026-06-17 19:32 ` [PATCH v4] " Andrew Bailey
2026-07-03 7:46 ` Luca Vizzarro
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox