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 4F030456F4; Tue, 30 Jul 2024 16:41:35 +0200 (CEST) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id BB6D740667; Tue, 30 Jul 2024 16:41:29 +0200 (CEST) Received: from mail-il1-f226.google.com (mail-il1-f226.google.com [209.85.166.226]) by mails.dpdk.org (Postfix) with ESMTP id C88FF400D7 for ; Tue, 30 Jul 2024 15:37:14 +0200 (CEST) Received: by mail-il1-f226.google.com with SMTP id e9e14a558f8ab-397cdb4bf11so9237055ab.2 for ; Tue, 30 Jul 2024 06:37:14 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=iol.unh.edu; s=unh-iol; t=1722346634; x=1722951434; darn=dpdk.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=Nf/TUSmci5A5sEk7GLJ5ZBcC50UEcKuXb/xte3rqinU=; b=EHrZKPXrahbheohY6r1lhXnXzT5SnNMHdGxC3M8BQHThKSccWAVTihVVegZodJ//HK ztSIbexftD8B57nb+6Wv8r0bUB+aY/JHhlrI2OiioaOV2zZCGrmMH6mg5eTzXNFyRcwl E/Fx0/EGeN5k5/OD5DH47T26jaQxKnVEIPUFs= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1722346634; x=1722951434; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=Nf/TUSmci5A5sEk7GLJ5ZBcC50UEcKuXb/xte3rqinU=; b=vROoSn5YpC2Z9XCGGgGfSBWtPRtyUZ/0A5FjqPoczM8bL0TFm5ovF+aSnHSHlQWMAb goXRF0vb6BTu3eSHBldi5XF/IBqv31Ew2vNxeZq5xoY/QBIxb0jrOfCyI6AZz1DONEcO EbfmEbIp9vtflwA4FsgUazMr5Ris1BODrCdauPSGPEKE8e0L4ZiSQxGja+rfhBrsdtWL NM5w+ksxPXBG07lpf5lEM8JTToR8AVq9wgCJTImA+aF48ef9gGwlPY9/K4SSiVVDuyWx ALZtlZiBEd0iNbdcef/u10fwtbG4+nKg3HfmyXYi9tVM3kJFZNtfNGtZi4psZgnikXju Ya7w== X-Gm-Message-State: AOJu0Yz7UeqNC3nLZ4PAXZE03NP1LkxObYMhFb7y+RVOVHXefh356Lgz MT51FFBkKGYrJu5OG1AAU7YmMFXTlTrsSEYei7e8MUWB4kjc6kyK5CuhTCOjs80yx+VL+MOGkN3 yrysp9uuet1vIXu2qJczM0c3NJsrjucQh X-Google-Smtp-Source: AGHT+IEFpB/v2lAKCWcjEdY+whznwqfuLoWe422SVjwn7RQEZOtiecPWkW5bm2z0m76st4q36ZFDHt4myo7W X-Received: by 2002:a05:6e02:152d:b0:382:64d9:1cba with SMTP id e9e14a558f8ab-39aec40d3e8mr120413535ab.19.1722346634024; Tue, 30 Jul 2024 06:37:14 -0700 (PDT) Received: from postal.iol.unh.edu (postal.iol.unh.edu. [132.177.123.84]) by smtp-relay.gmail.com with ESMTPS id 41be03b00d2f7-7a9f7d7abc0sm517375a12.14.2024.07.30.06.37.13 (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128); Tue, 30 Jul 2024 06:37:13 -0700 (PDT) X-Relaying-Domain: iol.unh.edu Received: from iol.unh.edu (unknown [10.31.1.133]) by postal.iol.unh.edu (Postfix) with ESMTP id D987060526E2; Tue, 30 Jul 2024 09:37:12 -0400 (EDT) From: jspewock@iol.unh.edu To: yoan.picchi@foss.arm.com, probb@iol.unh.edu, paul.szczepanek@arm.com, npratte@iol.unh.edu, thomas@monjalon.net, Honnappa.Nagarahalli@arm.com, Luca.Vizzarro@arm.com, juraj.linkes@pantheon.tech, wathsala.vithanage@arm.com Cc: dev@dpdk.org, Jeremy Spewock Subject: [PATCH v2 1/1] dts: add text parser for testpmd verbose output Date: Tue, 30 Jul 2024 09:34:59 -0400 Message-ID: <20240730133459.21907-2-jspewock@iol.unh.edu> X-Mailer: git-send-email 2.45.2 In-Reply-To: <20240730133459.21907-1-jspewock@iol.unh.edu> References: <20240729203955.267942-1-jspewock@iol.unh.edu> <20240730133459.21907-1-jspewock@iol.unh.edu> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit 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 From: Jeremy Spewock Multiple test suites from the old DTS framework rely on being able to consume and interpret the verbose output of testpmd. The new framework doesn't have an elegant way for handling the verbose output, but test suites are starting to be written that rely on it. This patch creates a TextParser class that can be used to extract the verbose information from any testpmd output and also adjusts the `stop` method of the shell to return all output that it collected. Signed-off-by: Jeremy Spewock --- One thing to note here is I don't love the regex in extract_verbose_output(). It works great when there is a bunch of verbose output in a row, but any chunk that isn't followed by another piece of verbose output will contain everything that comes after it in the match group. This could be solved by changing the regex to look ahead only for the next port X/queue Y line instead of also including the end of the string, and then having another alternate route which is solely dedicated to the last block of verbose output which greedily consumes everything until the end of ol_flags, but I didn't want to over complicate the regex since the text parser will extract the specific information it needs anyways. For reference, I was thinking it could be something like this: r"(port \d+/queue \d+:.*?(?=port \d+/queue \d+)|port \d+/queue \d+:.*ol_flags: [\w ]+)" but this has a lot of repition (some of which that could be ripped out with a simple variable) and it is a little more confusing to read I think. dts/framework/parser.py | 30 ++++ dts/framework/remote_session/testpmd_shell.py | 146 +++++++++++++++++- dts/framework/utils.py | 1 + 3 files changed, 175 insertions(+), 2 deletions(-) diff --git a/dts/framework/parser.py b/dts/framework/parser.py index 741dfff821..0b39025a48 100644 --- a/dts/framework/parser.py +++ b/dts/framework/parser.py @@ -160,6 +160,36 @@ def _find(text: str) -> Any: return ParserFn(TextParser_fn=_find) + @staticmethod + def find_all( + pattern: str | re.Pattern[str], + flags: re.RegexFlag = re.RegexFlag(0), + ) -> ParserFn: + """Makes a parser function that finds all of the regular expression matches in the text. + + If there are no matches found in the text than None will be returned, otherwise a list + containing all matches will be returned. Patterns that contain multiple groups will pack + the matches for each group into a tuple. + + Args: + pattern: The regular expression pattern. + flags: The regular expression flags. Ignored if the given pattern is already compiled. + + Returns: + A :class:`ParserFn` that can be used as metadata for a dataclass field. + """ + if isinstance(pattern, str): + pattern = re.compile(pattern, flags) + + def _find_all(text: str) -> list[str] | None: + m = pattern.findall(text) + if len(m) == 0: + return None + + return m + + return ParserFn(TextParser_fn=_find_all) + @staticmethod def find_int( pattern: str | re.Pattern[str], diff --git a/dts/framework/remote_session/testpmd_shell.py b/dts/framework/remote_session/testpmd_shell.py index 43e9f56517..dedf1553cf 100644 --- a/dts/framework/remote_session/testpmd_shell.py +++ b/dts/framework/remote_session/testpmd_shell.py @@ -31,7 +31,7 @@ from framework.settings import SETTINGS from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList from framework.testbed_model.sut_node import SutNode -from framework.utils import StrEnum +from framework.utils import REGEX_FOR_MAC_ADDRESS, StrEnum class TestPmdDevice: @@ -577,6 +577,128 @@ class TestPmdPortStats(TextParser): tx_bps: int = field(metadata=TextParser.find_int(r"Tx-bps:\s+(\d+)")) +class VerboseOLFlag(Flag): + """Flag representing the OL flags of a packet from Testpmd verbose output.""" + + #: + RTE_MBUF_F_RX_RSS_HASH = auto() + + #: + RTE_MBUF_F_RX_L4_CKSUM_GOOD = auto() + #: + RTE_MBUF_F_RX_L4_CKSUM_BAD = auto() + #: + RTE_MBUF_F_RX_L4_CKSUM_UNKNOWN = auto() + + #: + RTE_MBUF_F_RX_IP_CKSUM_GOOD = auto() + #: + RTE_MBUF_F_RX_IP_CKSUM_BAD = auto() + #: + RTE_MBUF_F_RX_IP_CKSUM_UNKNOWN = auto() + + #: + RTE_MBUF_F_RX_OUTER_L4_CKSUM_GOOD = auto() + #: + RTE_MBUF_F_RX_OUTER_L4_CKSUM_BAD = auto() + #: + RTE_MBUF_F_RX_OUTER_L4_CKSUM_UNKNOWN = auto() + + @classmethod + def from_str_list(cls, arr: list[str]) -> Self: + """Makes an instance from a list containing the flag members. + + Args: + arr: A list of strings containing ol_flag values. + + Returns: + A new instance of the flag. + """ + flag = cls(0) + for name in cls.__members__: + if name in arr: + flag |= 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.wrap(TextParser.find(r"ol_flags: ([^\n]+)"), str.split), + cls.from_str_list, + ) + + +@dataclass +class TestPmdVerbosePacket(TextParser): + """Packet information provided by verbose output in Testpmd. + + The "receive/sent queue" information is not included in this dataclass because this value is + captured on the outer layer of input found in :class:`TestPmdVerboseOutput`. + """ + + #: + src_mac: str = field(metadata=TextParser.find(r"src=({})".format(REGEX_FOR_MAC_ADDRESS))) + #: + dst_mac: str = field(metadata=TextParser.find(r"dst=({})".format(REGEX_FOR_MAC_ADDRESS))) + #: Memory pool the packet was handled on. + pool: str = field(metadata=TextParser.find(r"pool=(\S+)")) + #: Packet type in hex. + p_type: int = field(metadata=TextParser.find_int(r"type=(0x[a-fA-F\d]+)")) + #: + length: int = field(metadata=TextParser.find_int(r"length=(\d+)")) + #: Number of segments in the packet. + nb_segs: int = field(metadata=TextParser.find_int(r"nb_segs=(\d+)")) + #: Hardware packet type. Collected as a string delimited by whitespace. + hw_ptype: str = field(metadata=TextParser.find(r"hw ptype: ([^-]+)")) + #: Software packet type. Collected as a string delimited by whitespace. + sw_ptype: str = field(metadata=TextParser.find(r"sw ptype: ([^-]+)")) + #: + l2_len: int = field(metadata=TextParser.find_int(r"l2_len=(\d+)")) + #: + ol_flags: VerboseOLFlag = field(metadata=VerboseOLFlag.make_parser()) + #: RSS has of the packet in hex. + rss_hash: int | None = field( + default=None, metadata=TextParser.find_int(r"RSS hash=(0x[a-fA-F\d]+)") + ) + #: RSS queue that handled the packet in hex. + rss_queue: int | None = field( + default=None, metadata=TextParser.find_int(r"RSS queue=(0x[a-fA-F\d]+)") + ) + #: + l3_len: int | None = field(default=None, metadata=TextParser.find_int(r"l3_len=(\d+)")) + #: + l4_len: int | None = field(default=None, metadata=TextParser.find_int(r"l4_len=(\d+)")) + + +@dataclass +class TestPmdVerboseOutput(TextParser): + """Verbose output generated by Testpmd. + + This class is the top level of the output, containing verbose output delimited by + "port X/queue Y: sent/received Z packets". + """ + + #: ID of the port that handled the packet. + port_id: int = field(metadata=TextParser.find_int(r"port (\d+)/queue \d+")) + #: ID of the queue that handled the packet. + queue_id: int = field(metadata=TextParser.find_int(r"port \d+/queue (\d+)")) + #: Whether the packet was received or sent by the queue/port. + was_received: bool = field(metadata=TextParser.find(r"received \d+ packets")) + #: List of packets handed by the port/queue in this section. + packets: list[TestPmdVerbosePacket] = field( + metadata=TextParser.wrap( + TextParser.find_all(r"(src=[\w\s=:-]+ol_flags: [\w ]+)"), + lambda matches_arr: list(map(TestPmdVerbosePacket.parse, matches_arr)), + ) + ) + + class TestPmdShell(DPDKShell): """Testpmd interactive shell. @@ -645,7 +767,7 @@ def start(self, verify: bool = True) -> None: "Not all ports came up after starting packet forwarding in testpmd." ) - def stop(self, verify: bool = True) -> None: + def stop(self, verify: bool = True) -> str: """Stop packet forwarding. Args: @@ -656,6 +778,9 @@ def stop(self, verify: bool = True) -> None: Raises: InteractiveCommandExecutionError: If `verify` is :data:`True` and the command to stop forwarding results in an error. + + Returns: + Output gathered from sending the stop command. """ stop_cmd_output = self.send_command("stop") if verify: @@ -665,6 +790,7 @@ def stop(self, verify: bool = True) -> None: ): self._logger.debug(f"Failed to stop packet forwarding: \n{stop_cmd_output}") raise InteractiveCommandExecutionError("Testpmd failed to stop packet forwarding.") + return stop_cmd_output def get_devices(self) -> list[TestPmdDevice]: """Get a list of device names that are known to testpmd. @@ -806,6 +932,22 @@ def show_port_stats(self, port_id: int) -> TestPmdPortStats: return TestPmdPortStats.parse(output) + @staticmethod + def extract_verbose_output(output: str) -> list[TestPmdVerboseOutput]: + """Extract the verbose information present in given testpmd output. + + This method extracts sections of verbose output that begin with the line + "port X/queue Y: sent/received Z packets" and end with the ol_flags of a packet. + + Args: + output: Testpmd output that contains verbose information + + Returns: + List of parsed packet information gathered from verbose information in `output`. + """ + iter = re.finditer(r"(port \d+/queue \d+:.*?(?=port \d+/queue \d+|$))", output, re.S) + return [TestPmdVerboseOutput.parse(s.group(0)) for s in iter] + def _close(self) -> None: """Overrides :meth:`~.interactive_shell.close`.""" self.stop() diff --git a/dts/framework/utils.py b/dts/framework/utils.py index 6b5d5a805f..9c64cf497f 100644 --- a/dts/framework/utils.py +++ b/dts/framework/utils.py @@ -27,6 +27,7 @@ from .exception import ConfigurationError REGEX_FOR_PCI_ADDRESS: str = "/[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}.[0-9]{1}/" +REGEX_FOR_MAC_ADDRESS: str = r"(?:[\da-fA-F]{2}:){5}[\da-fA-F]{2}" def expand_range(range_str: str) -> list[int]: -- 2.45.2