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 5169B4545F; Fri, 14 Jun 2024 19:36:32 +0200 (CEST) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id D33944279E; Fri, 14 Jun 2024 19:36:31 +0200 (CEST) Received: from mail-lj1-f182.google.com (mail-lj1-f182.google.com [209.85.208.182]) by mails.dpdk.org (Postfix) with ESMTP id 71EB940B9A for ; Fri, 14 Jun 2024 19:36:30 +0200 (CEST) Received: by mail-lj1-f182.google.com with SMTP id 38308e7fff4ca-2ebfb526d49so2617681fa.1 for ; Fri, 14 Jun 2024 10:36:30 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=iol.unh.edu; s=unh-iol; t=1718386590; x=1718991390; 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=YGnn7iG3Yx6HB9fKNS8vragWeVYwQ7lo6xwKIA4+d7Y=; b=A3yP9jSrb9gBy78yK49+gqjvJKNHv+Q+2asz/RNaDQ6HGZLVble+PSreqMtV4EiAfI R9/HLma32o9ZPefYb7o7jBsQ+g8cBgtJi/3ZuHXlcQ48zM6x35uYpgBVWc23g2Lo7frj nDIGTeqhh6/obm+QCKQUboB8sa94JRpi4v13w= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1718386590; x=1718991390; 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=YGnn7iG3Yx6HB9fKNS8vragWeVYwQ7lo6xwKIA4+d7Y=; b=vbEeNjOSMMGqzc+BtxPnUHppJshTMDsOY8tyK/wyYDl7G7d7DtKIdYWx5Y7J9H/V+V 6eSATyH1+7vnDnCND7TTkfYu+d/WrlW+DlXckqczTn7ZROCg3g7NZFN0vYVxoKvxHvfH GpB4GXzjpV1bWnwxbN6Jnj8w/kekTLxOkldcfbSRTQCePncq+rKvOJvuI6DkTCmxkyXt trByBqy15zSVnMRQCTwexhOnjJsVAXhWUDqqiZgR2P/Z1vR910LsstCaOV4i3PR4dPCA mlKoKC8G3tCNTv2fL9+xq+3rv5ePrktFuJj1KwEEg1rMdnh4bS1WrEKmUmOs+iBdq0FV H+sA== X-Gm-Message-State: AOJu0YxLwOrY0vWM4bmNi+6M/g3upqQxTYZ2sItTLiyO6mxVPaNyBGh2 tFb/xOZPvnqvvfer86MGnAWrFaQIJLlV77/iqB5H+gMgdDDU0srxud1brmpmmny05TvfOLgnfCR W05QtJ0R2AHZKOKU+GW0VjUNKZym2L/ROx8BBBQ== X-Google-Smtp-Source: AGHT+IEB+UmgdYMNnKgb2EoxESsqEEXW12+y/QtwY/Bvfi7JY6xUWZCu+w6jVRGr0NeaqzhHHfit5q1jH40mIvOtqc4= X-Received: by 2002:a05:651c:154a:b0:2ec:143e:2893 with SMTP id 38308e7fff4ca-2ec143e2926mr18923131fa.2.1718386589807; Fri, 14 Jun 2024 10:36:29 -0700 (PDT) MIME-Version: 1.0 References: <20240412111136.3470304-1-luca.vizzarro@arm.com> <20240606213420.254260-1-luca.vizzarro@arm.com> <20240606213420.254260-5-luca.vizzarro@arm.com> In-Reply-To: <20240606213420.254260-5-luca.vizzarro@arm.com> From: Nicholas Pratte Date: Fri, 14 Jun 2024 13:36:18 -0400 Message-ID: Subject: Re: [PATCH v5 4/5] dts: add `show port info` command to TestPmdShell To: Luca Vizzarro Cc: dev@dpdk.org, Jeremy Spewock , =?UTF-8?Q?Juraj_Linke=C5=A1?= , Paul Szczepanek 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 Tested-by: Nicholas Pratte Reviewed-by: Nicholas Pratte On Thu, Jun 6, 2024 at 5:34=E2=80=AFPM Luca Vizzarro wrote: > > Add a new TestPmdPort data structure to represent the output > returned by `show port info`, which is implemented as part of > TestPmdShell. > > The TestPmdPort data structure and its derived classes are modelled > based on the relevant testpmd source code. > > This implementation makes extensive use of regular expressions, which > all parse individually. The rationale behind this is to lower the risk > of the testpmd output changing as part of development. Therefore > minimising breakage. > > Bugzilla ID: 1407 > > Signed-off-by: Luca Vizzarro > Reviewed-by: Paul Szczepanek > --- > dts/framework/remote_session/testpmd_shell.py | 537 +++++++++++++++++- > 1 file changed, 536 insertions(+), 1 deletion(-) > > diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framewor= k/remote_session/testpmd_shell.py > index cb2ab6bd00..ab9a1f86a9 100644 > --- a/dts/framework/remote_session/testpmd_shell.py > +++ b/dts/framework/remote_session/testpmd_shell.py > @@ -1,6 +1,7 @@ > # SPDX-License-Identifier: BSD-3-Clause > # Copyright(c) 2023 University of New Hampshire > # Copyright(c) 2023 PANTHEON.tech s.r.o. > +# Copyright(c) 2024 Arm Limited > > """Testpmd interactive shell. > > @@ -15,12 +16,17 @@ > testpmd_shell.close() > """ > > +import re > import time > -from enum import auto > +from dataclasses import dataclass, field > +from enum import Flag, auto > from pathlib import PurePath > from typing import Callable, ClassVar > > +from typing_extensions import Self > + > from framework.exception import InteractiveCommandExecutionError > +from framework.parser import ParserFn, TextParser > from framework.settings import SETTINGS > from framework.utils import StrEnum > > @@ -80,6 +86,491 @@ class TestPmdForwardingModes(StrEnum): > recycle_mbufs =3D auto() > > > +class VLANOffloadFlag(Flag): > + """Flag representing the VLAN offload settings of a NIC port.""" > + > + #: > + STRIP =3D auto() > + #: > + FILTER =3D auto() > + #: > + EXTEND =3D auto() > + #: > + QINQ_STRIP =3D auto() > + > + @classmethod > + def from_str_dict(cls, d): > + """Makes an instance from a dict containing the flag member name= s with an "on" value. > + > + Args: > + d: A dictionary containing the flag members as keys and any = string value. > + > + Returns: > + A new instance of the flag. > + """ > + flag =3D cls(0) > + for name in cls.__members__: > + if d.get(name) =3D=3D "on": > + flag |=3D cls[name] > + return flag > + > + @classmethod > + def make_parser(cls) -> ParserFn: > + """Makes a parser function. > + > + Returns: > + ParserFn: A dictionary for the `dataclasses.field` metadata = argument containing a > + parser function that makes an instance of this flag from= text. > + """ > + return TextParser.wrap( > + TextParser.find( > + r"VLAN offload:\s+" > + r"strip (?Pon|off), " > + r"filter (?Pon|off), " > + r"extend (?Pon|off), " > + r"qinq strip (?Pon|off)$", > + re.MULTILINE, > + named=3DTrue, > + ), > + cls.from_str_dict, > + ) > + > + > +class RSSOffloadTypesFlag(Flag): > + """Flag representing the RSS offload flow types supported by the NIC= port.""" > + > + #: > + ipv4 =3D auto() > + #: > + ipv4_frag =3D auto() > + #: > + ipv4_tcp =3D auto() > + #: > + ipv4_udp =3D auto() > + #: > + ipv4_sctp =3D auto() > + #: > + ipv4_other =3D auto() > + #: > + ipv6 =3D auto() > + #: > + ipv6_frag =3D auto() > + #: > + ipv6_tcp =3D auto() > + #: > + ipv6_udp =3D auto() > + #: > + ipv6_sctp =3D auto() > + #: > + ipv6_other =3D auto() > + #: > + l2_payload =3D auto() > + #: > + ipv6_ex =3D auto() > + #: > + ipv6_tcp_ex =3D auto() > + #: > + ipv6_udp_ex =3D auto() > + #: > + port =3D auto() > + #: > + vxlan =3D auto() > + #: > + geneve =3D auto() > + #: > + nvgre =3D auto() > + #: > + user_defined_22 =3D auto() > + #: > + gtpu =3D auto() > + #: > + eth =3D auto() > + #: > + s_vlan =3D auto() > + #: > + c_vlan =3D auto() > + #: > + esp =3D auto() > + #: > + ah =3D auto() > + #: > + l2tpv3 =3D auto() > + #: > + pfcp =3D auto() > + #: > + pppoe =3D auto() > + #: > + ecpri =3D auto() > + #: > + mpls =3D auto() > + #: > + ipv4_chksum =3D auto() > + #: > + l4_chksum =3D auto() > + #: > + l2tpv2 =3D auto() > + #: > + ipv6_flow_label =3D auto() > + #: > + user_defined_38 =3D auto() > + #: > + user_defined_39 =3D auto() > + #: > + user_defined_40 =3D auto() > + #: > + user_defined_41 =3D auto() > + #: > + user_defined_42 =3D auto() > + #: > + user_defined_43 =3D auto() > + #: > + user_defined_44 =3D auto() > + #: > + user_defined_45 =3D auto() > + #: > + user_defined_46 =3D auto() > + #: > + user_defined_47 =3D auto() > + #: > + user_defined_48 =3D auto() > + #: > + user_defined_49 =3D auto() > + #: > + user_defined_50 =3D auto() > + #: > + user_defined_51 =3D auto() > + #: > + l3_pre96 =3D auto() > + #: > + l3_pre64 =3D auto() > + #: > + l3_pre56 =3D auto() > + #: > + l3_pre48 =3D auto() > + #: > + l3_pre40 =3D auto() > + #: > + l3_pre32 =3D auto() > + #: > + l2_dst_only =3D auto() > + #: > + l2_src_only =3D auto() > + #: > + l4_dst_only =3D auto() > + #: > + l4_src_only =3D auto() > + #: > + l3_dst_only =3D auto() > + #: > + l3_src_only =3D auto() > + > + #: > + ip =3D ipv4 | ipv4_frag | ipv4_other | ipv6 | ipv6_frag | ipv6_other= | ipv6_ex > + #: > + udp =3D ipv4_udp | ipv6_udp | ipv6_udp_ex > + #: > + tcp =3D ipv4_tcp | ipv6_tcp | ipv6_tcp_ex > + #: > + sctp =3D ipv4_sctp | ipv6_sctp > + #: > + tunnel =3D vxlan | geneve | nvgre > + #: > + vlan =3D s_vlan | c_vlan > + #: > + all =3D ( > + eth > + | vlan > + | ip > + | tcp > + | udp > + | sctp > + | l2_payload > + | l2tpv3 > + | esp > + | ah > + | pfcp > + | gtpu > + | ecpri > + | mpls > + | l2tpv2 > + ) > + > + @classmethod > + def from_list_string(cls, names: str) -> Self: > + """Makes a flag from a whitespace-separated list of names. > + > + Args: > + names: a whitespace-separated list containing the members of= this flag. > + > + Returns: > + An instance of this flag. > + """ > + flag =3D cls(0) > + for name in names.split(): > + flag |=3D cls.from_str(name) > + return flag > + > + @classmethod > + def from_str(cls, name: str) -> Self: > + """Makes a flag matching the supplied name. > + > + Args: > + name: a valid member of this flag in text > + Returns: > + An instance of this flag. > + """ > + member_name =3D name.strip().replace("-", "_") > + return cls[member_name] > + > + @classmethod > + def make_parser(cls) -> ParserFn: > + """Makes a parser function. > + > + Returns: > + ParserFn: A dictionary for the `dataclasses.field` metadata = argument containing a > + parser function that makes an instance of this flag from= text. > + """ > + return TextParser.wrap( > + TextParser.find(r"Supported RSS offload flow types:((?:\r?\n= ? \S+)+)", re.MULTILINE), > + RSSOffloadTypesFlag.from_list_string, > + ) > + > + > +class DeviceCapabilitiesFlag(Flag): > + """Flag representing the device capabilities.""" > + > + #: Device supports Rx queue setup after device started. > + RUNTIME_RX_QUEUE_SETUP =3D auto() > + #: Device supports Tx queue setup after device started. > + RUNTIME_TX_QUEUE_SETUP =3D auto() > + #: Device supports shared Rx queue among ports within Rx domain and = switch domain. > + RXQ_SHARE =3D auto() > + #: Device supports keeping flow rules across restart. > + FLOW_RULE_KEEP =3D auto() > + #: Device supports keeping shared flow objects across restart. > + FLOW_SHARED_OBJECT_KEEP =3D auto() > + > + @classmethod > + def make_parser(cls) -> ParserFn: > + """Makes a parser function. > + > + Returns: > + ParserFn: A dictionary for the `dataclasses.field` metadata = argument containing a > + parser function that makes an instance of this flag from= text. > + """ > + return TextParser.wrap( > + TextParser.find_int(r"Device capabilities: (0x[A-Fa-f\d]+)")= , > + cls, > + ) > + > + > +class DeviceErrorHandlingMode(StrEnum): > + """Enum representing the device error handling mode.""" > + > + #: > + none =3D auto() > + #: > + passive =3D auto() > + #: > + proactive =3D auto() > + #: > + unknown =3D auto() > + > + @classmethod > + def make_parser(cls) -> ParserFn: > + """Makes a parser function. > + > + Returns: > + ParserFn: A dictionary for the `dataclasses.field` metadata = argument containing a > + parser function that makes an instance of this enum from= text. > + """ > + return TextParser.wrap(TextParser.find(r"Device error handling m= ode: (\w+)"), cls) > + > + > +def make_device_private_info_parser() -> ParserFn: > + """Device private information parser. > + > + Ensures that we are not parsing invalid device private info output. > + > + Returns: > + ParserFn: A dictionary for the `dataclasses.field` metadata argu= ment containing a parser > + function that parses the device private info from the TestPm= d port info output. > + """ > + > + def _validate(info: str): > + info =3D info.strip() > + if info =3D=3D "none" or info.startswith("Invalid file") or info= .startswith("Failed to dump"): > + return None > + return info > + > + return TextParser.wrap(TextParser.find(r"Device private info:\s+([\s= \S]+)"), _validate) > + > + > +@dataclass > +class TestPmdPort(TextParser): > + """Dataclass representing the result of testpmd's ``show port info``= command.""" > + > + #: > + id: int =3D field(metadata=3DTextParser.find_int(r"Infos for port (\= d+)\b")) > + #: > + device_name: str =3D field(metadata=3DTextParser.find(r"Device name:= ([^\r\n]+)")) > + #: > + driver_name: str =3D field(metadata=3DTextParser.find(r"Driver name:= ([^\r\n]+)")) > + #: > + socket_id: int =3D field(metadata=3DTextParser.find_int(r"Connect to= socket: (\d+)")) > + #: > + is_link_up: bool =3D field(metadata=3DTextParser.find("Link status: = up")) > + #: > + link_speed: str =3D field(metadata=3DTextParser.find(r"Link speed: (= [^\r\n]+)")) > + #: > + is_link_full_duplex: bool =3D field(metadata=3DTextParser.find("Link= duplex: full-duplex")) > + #: > + is_link_autonegotiated: bool =3D field(metadata=3DTextParser.find("A= utoneg status: On")) > + #: > + is_promiscuous_mode_enabled: bool =3D field(metadata=3DTextParser.fi= nd("Promiscuous mode: enabled")) > + #: > + is_allmulticast_mode_enabled: bool =3D field( > + metadata=3DTextParser.find("Allmulticast mode: enabled") > + ) > + #: Maximum number of MAC addresses > + max_mac_addresses_num: int =3D field( > + metadata=3DTextParser.find_int(r"Maximum number of MAC addresses= : (\d+)") > + ) > + #: Maximum configurable length of RX packet > + max_hash_mac_addresses_num: int =3D field( > + metadata=3DTextParser.find_int(r"Maximum number of MAC addresses= of hash filtering: (\d+)") > + ) > + #: Minimum size of RX buffer > + min_rx_bufsize: int =3D field(metadata=3DTextParser.find_int(r"Minim= um size of RX buffer: (\d+)")) > + #: Maximum configurable length of RX packet > + max_rx_packet_length: int =3D field( > + metadata=3DTextParser.find_int(r"Maximum configurable length of = RX packet: (\d+)") > + ) > + #: Maximum configurable size of LRO aggregated packet > + max_lro_packet_size: int =3D field( > + metadata=3DTextParser.find_int(r"Maximum configurable size of LR= O aggregated packet: (\d+)") > + ) > + > + #: Current number of RX queues > + rx_queues_num: int =3D field(metadata=3DTextParser.find_int(r"Curren= t number of RX queues: (\d+)")) > + #: Max possible RX queues > + max_rx_queues_num: int =3D field(metadata=3DTextParser.find_int(r"Ma= x possible RX queues: (\d+)")) > + #: Max possible number of RXDs per queue > + max_queue_rxd_num: int =3D field( > + metadata=3DTextParser.find_int(r"Max possible number of RXDs per= queue: (\d+)") > + ) > + #: Min possible number of RXDs per queue > + min_queue_rxd_num: int =3D field( > + metadata=3DTextParser.find_int(r"Min possible number of RXDs per= queue: (\d+)") > + ) > + #: RXDs number alignment > + rxd_alignment_num: int =3D field(metadata=3DTextParser.find_int(r"RX= Ds number alignment: (\d+)")) > + > + #: Current number of TX queues > + tx_queues_num: int =3D field(metadata=3DTextParser.find_int(r"Curren= t number of TX queues: (\d+)")) > + #: Max possible TX queues > + max_tx_queues_num: int =3D field(metadata=3DTextParser.find_int(r"Ma= x possible TX queues: (\d+)")) > + #: Max possible number of TXDs per queue > + max_queue_txd_num: int =3D field( > + metadata=3DTextParser.find_int(r"Max possible number of TXDs per= queue: (\d+)") > + ) > + #: Min possible number of TXDs per queue > + min_queue_txd_num: int =3D field( > + metadata=3DTextParser.find_int(r"Min possible number of TXDs per= queue: (\d+)") > + ) > + #: TXDs number alignment > + txd_alignment_num: int =3D field(metadata=3DTextParser.find_int(r"TX= Ds number alignment: (\d+)")) > + #: Max segment number per packet > + max_packet_segment_num: int =3D field( > + metadata=3DTextParser.find_int(r"Max segment number per packet: = (\d+)") > + ) > + #: Max segment number per MTU/TSO > + max_mtu_segment_num: int =3D field( > + metadata=3DTextParser.find_int(r"Max segment number per MTU\/TSO= : (\d+)") > + ) > + > + #: > + device_capabilities: DeviceCapabilitiesFlag =3D field( > + metadata=3DDeviceCapabilitiesFlag.make_parser(), > + ) > + #: > + device_error_handling_mode: DeviceErrorHandlingMode =3D field( > + metadata=3DDeviceErrorHandlingMode.make_parser() > + ) > + #: > + device_private_info: str | None =3D field( > + default=3DNone, > + metadata=3Dmake_device_private_info_parser(), > + ) > + > + #: > + hash_key_size: int | None =3D field( > + default=3DNone, metadata=3DTextParser.find_int(r"Hash key size i= n bytes: (\d+)") > + ) > + #: > + redirection_table_size: int | None =3D field( > + default=3DNone, metadata=3DTextParser.find_int(r"Redirection tab= le size: (\d+)") > + ) > + #: > + supported_rss_offload_flow_types: RSSOffloadTypesFlag =3D field( > + default=3DRSSOffloadTypesFlag(0), metadata=3DRSSOffloadTypesFlag= .make_parser() > + ) > + > + #: > + mac_address: str | None =3D field( > + default=3DNone, metadata=3DTextParser.find(r"MAC address: ([A-Fa= -f0-9:]+)") > + ) > + #: > + fw_version: str | None =3D field( > + default=3DNone, metadata=3DTextParser.find(r"Firmware-version: (= [^\r\n]+)") > + ) > + #: > + dev_args: str | None =3D field(default=3DNone, metadata=3DTextParser= .find(r"Devargs: ([^\r\n]+)")) > + #: Socket id of the memory allocation > + mem_alloc_socket_id: int | None =3D field( > + default=3DNone, > + metadata=3DTextParser.find_int(r"memory allocation on the socket= : (\d+)"), > + ) > + #: > + mtu: int | None =3D field(default=3DNone, metadata=3DTextParser.find= _int(r"MTU: (\d+)")) > + > + #: > + vlan_offload: VLANOffloadFlag | None =3D field( > + default=3DNone, > + metadata=3DVLANOffloadFlag.make_parser(), > + ) > + > + #: Maximum size of RX buffer > + max_rx_bufsize: int | None =3D field( > + default=3DNone, metadata=3DTextParser.find_int(r"Maximum size of= RX buffer: (\d+)") > + ) > + #: Maximum number of VFs > + max_vfs_num: int | None =3D field( > + default=3DNone, metadata=3DTextParser.find_int(r"Maximum number = of VFs: (\d+)") > + ) > + #: Maximum number of VMDq pools > + max_vmdq_pools_num: int | None =3D field( > + default=3DNone, metadata=3DTextParser.find_int(r"Maximum number = of VMDq pools: (\d+)") > + ) > + > + #: > + switch_name: str | None =3D field( > + default=3DNone, metadata=3DTextParser.find(r"Switch name: ([\r\n= ]+)") > + ) > + #: > + switch_domain_id: int | None =3D field( > + default=3DNone, metadata=3DTextParser.find_int(r"Switch domain I= d: (\d+)") > + ) > + #: > + switch_port_id: int | None =3D field( > + default=3DNone, metadata=3DTextParser.find_int(r"Switch Port Id:= (\d+)") > + ) > + #: > + switch_rx_domain: int | None =3D field( > + default=3DNone, metadata=3DTextParser.find_int(r"Switch Rx domai= n: (\d+)") > + ) > + > + > class TestPmdShell(InteractiveShell): > """Testpmd interactive shell. > > @@ -225,6 +716,50 @@ def set_forward_mode(self, mode: TestPmdForwardingMo= des, verify: bool =3D True): > f"Test pmd failed to set fwd mode to {mode.value}" > ) > > + def show_port_info_all(self) -> list[TestPmdPort]: > + """Returns the information of all the ports. > + > + Returns: > + list[TestPmdPort]: A list containing all the ports informati= on as `TestPmdPort`. > + """ > + output =3D self.send_command("show port info all") > + > + # Sample output of the "all" command looks like: > + # > + # > + # > + # ********************* Infos for port 0 ********************* > + # Key: value > + # > + # ********************* Infos for port 1 ********************* > + # Key: value > + # > + # > + # Takes advantage of the double new line in between ports as end= delimiter. But we need to > + # artificially add a new line at the end to pick up the last por= t. Because commands are > + # executed on a pseudo-terminal created by paramiko on the remot= e node, lines end with CRLF. > + # Therefore we also need to take the carriage return into accoun= t. > + iter =3D re.finditer(r"\*{21}.*?[\r\n]{4}", output + "\r\n", re.= S) > + return [TestPmdPort.parse(block.group(0)) for block in iter] > + > + def show_port_info(self, port_id: int) -> TestPmdPort: > + """Returns the given port information. > + > + Args: > + port_id: The port ID to gather information for. > + > + Raises: > + InteractiveCommandExecutionError: If `port_id` is invalid. > + > + Returns: > + TestPmdPort: An instance of `TestPmdPort` containing the giv= en port's information. > + """ > + output =3D self.send_command(f"show port info {port_id}", skip_f= irst_line=3DTrue) > + if output.startswith("Invalid port"): > + raise InteractiveCommandExecutionError("invalid port given") > + > + return TestPmdPort.parse(output) > + > def close(self) -> None: > """Overrides :meth:`~.interactive_shell.close`.""" > self.send_command("quit", "") > -- > 2.34.1 >