|WARNING| [v6] dts: report dut/NIC info during DTS run
dpdklab at iol.unh.edu
dpdklab at iol.unh.edu
Thu Jul 9 16:06:23 CEST 2026
Test-Label: ai-code-review
Test-Status: WARNING
https://dpdk.org/patch/166651
_AI Code Review_
=== Patch Review: series.patch (via Claude) ===
Review date: 2026-07-09
# DPDK Patch Review
## Summary
This patch adds NIC information gathering and JSON output to the DTS (DPDK Test Suite) framework. It collects hardware details about network interfaces and writes them to a file for use in results reporting.
---
## Errors
### 1. Error path resource leak - file not closed on exception
**Location:** `dts/framework/test_run.py:354-355`
The file is opened but not guaranteed to be closed if `json.dump()` raises an exception (e.g., serialization error).
```python
# BAD - file may leak on exception
with open(f"{SETTINGS.output_dir}/dut_info.json", "w") as file:
json.dump(testrun_nic_info, file, indent=3)
```
**Fix:** The `with` statement already ensures the file is closed. However, if `json.dump()` fails, the exception propagates without any cleanup logging or context. Consider wrapping in a try-except if you want to handle serialization failures gracefully:
```python
try:
with open(f"{SETTINGS.output_dir}/dut_info.json", "w") as file:
json.dump(testrun_nic_info, file, indent=3)
self.logger.info(f"DUT NIC info written to: {SETTINGS.output_dir}/dut_info.json")
except (OSError, TypeError) as e:
self.logger.error(f"Failed to write DUT NIC info: {e}")
raise ConfigurationError(f"Could not write NIC info: {e}") from e
```
---
### 2. Missing error check - `lshw_result.stdout` not validated before use
**Location:** `dts/framework/testbed_model/linux_session.py:224`
The code checks `lshw_result.stdout == ""` but does not verify that the command actually parsed the PCI address correctly. An empty stdout could mean the grep found nothing, but a non-empty stdout could still be garbage if the command syntax was wrong.
```python
if lshw_result.return_code != 0 or lshw_result.stdout == "":
raise ConfigurationError(f"Unable to get bus type for port {pci_addr}.")
bus_type = lshw_result.stdout
```
**Issue:** If `lshw_result.stdout` contains unexpected output (e.g., multiple lines, error message), `bus_type` will have an invalid value leading to incorrect `bus_info` construction on line 227.
**Fix:** Add validation that `bus_type` contains expected format (e.g., `"pci"`, `"usb"`):
```python
if lshw_result.return_code != 0 or not lshw_result.stdout.strip():
raise ConfigurationError(f"Unable to get bus type for port {pci_addr}.")
bus_type = lshw_result.stdout.strip()
if not bus_type:
raise ConfigurationError(f"Empty bus type returned for port {pci_addr}.")
```
---
### 3. Potential KeyError - `port_to_data[bus_info]` not checked before access
**Location:** `dts/framework/testbed_model/linux_session.py:228`
```python
nic_port: LshwOutput | None = port_to_data[bus_info]
```
**Issue:** If `bus_info` is not in `port_to_data`, this raises `KeyError`. The subsequent `if nic_port is None` check will never execute.
**Fix:** Use `.get()` to avoid KeyError:
```python
nic_port: LshwOutput | None = port_to_data.get(bus_info)
if nic_port is None:
raise ConfigurationError(f"Port {pci_addr} could not be found on the node.")
```
---
### 4. Inconsistent error handling - `ethtool` failure logged but not raised
**Location:** `dts/framework/testbed_model/linux_session.py:244-249`
When `ethtool` fails to get NIC speed, an error is logged but the function continues. However, other failures (lshw bus type, missing config) raise `ConfigurationError`. This inconsistency means partial NIC info is returned with `"Unknown"` speed, which may not be the intended behavior.
**Decision needed:** Should missing speed be a fatal error or a warning?
If fatal (consistent with other errors):
```python
if ethtool_result.return_code == 0 and ethtool_result.stdout:
nic_speed = ethtool_result.stdout
else:
raise ConfigurationError(f"Unable to get speed for NIC: {pci_addr}")
```
If acceptable (current behavior is fine), change to Warning-level or keep as-is. But the current mix of error and continue is confusing.
---
### 5. Type annotation mismatch - `nic_port` cannot be None after assignment
**Location:** `dts/framework/testbed_model/linux_session.py:228`
```python
nic_port: LshwOutput | None = port_to_data[bus_info]
if nic_port is None:
raise ConfigurationError(...)
```
**Issue:** The type hint allows `None`, but `port_to_data[bus_info]` will raise `KeyError` if the key is missing, not return `None`. This is dead code after fixing Error #3.
**Fix (after applying Error #3 fix):**
```python
nic_port = port_to_data.get(bus_info)
if nic_port is None:
raise ConfigurationError(f"Port {pci_addr} could not be found on the node.")
# nic_port is now guaranteed to be LshwOutput, no Optional needed below
```
---
## Warnings
### 1. Missing documentation - `get_nic_info()` abstract method has no Raises clause
**Location:** `dts/framework/testbed_model/os_session.py:584-592`
The abstract method docstring documents the return type but the `Raises:` section only mentions `ConfigurationError` generically. The implementation in `LinuxSession` raises `ConfigurationError` for multiple specific reasons, which should be documented in the abstract method.
**Suggested addition:**
```python
def get_nic_info(self) -> list[dict[str, str]]:
"""Get NIC information.
Returns:
NIC info as a list of dictionaries, one per configured port.
Each dictionary contains: make, model, hardware version,
firmware version, deviceBusType, deviceId, pmd, speed.
Raises:
ConfigurationError: If NIC information cannot be retrieved
(missing bus type, port not found, configuration unavailable,
or required fields missing).
"""
```
---
### 2. Hardcoded JSON filename and logging message could be parameterized
**Location:** `dts/framework/test_run.py:354-357`
The filename `"dut_info.json"` is hardcoded. If the format or purpose changes, it will need to be updated in multiple places.
**Suggestion:** Define a constant at module level:
```python
DUT_INFO_FILENAME = "dut_info.json"
# Then use:
with open(f"{SETTINGS.output_dir}/{DUT_INFO_FILENAME}", "w") as file:
json.dump(testrun_nic_info, file, indent=3)
self.logger.info(f"DUT NIC info written to: {SETTINGS.output_dir}/{DUT_INFO_FILENAME}")
```
---
### 3. Dictionary key strings inconsistent casing
**Location:** `dts/framework/testbed_model/linux_session.py:251-258`
The keys use inconsistent casing: `"make"`, `"model"` (lowercase) vs `"deviceBusType"`, `"deviceId"` (camelCase).
**Suggestion:** Use consistent naming convention (all snake_case or all camelCase). If this matches an external API schema, document that requirement. Otherwise, prefer snake_case for consistency with Python conventions:
```python
dut_json = {
"make": ...,
"model": ...,
"hardware_version": ...,
"firmware_version": ...,
"device_bus_type": bus_type,
"device_id": ...,
"pmd": ...,
"speed": nic_speed or "Unknown",
}
```
---
### 4. Shell command parsing is fragile
**Location:** `dts/framework/testbed_model/linux_session.py:223-224`
```python
f"sudo lshw -c network -businfo | grep '{pci_addr}' | cut -d'@' -f1"
```
**Issue:** This assumes the output format of `lshw` is stable and that `pci_addr` does not contain shell metacharacters. If `pci_addr` contains characters like `$`, `` ` ``, or `|`, the command could fail or behave unexpectedly.
**Suggestion:** Use proper escaping or parameterization. If `send_command` does not support parameterization, at least validate that `pci_addr` matches expected format (`[0-9a-f:.]+`):
```python
import re
if not re.match(r'^[0-9a-fA-F:.]+$', pci_addr):
raise ConfigurationError(f"Invalid PCI address format: {pci_addr}")
```
---
### 5. Missing validation - port configuration fields may be empty strings
**Location:** `dts/framework/testbed_model/linux_session.py:251-258`
The code checks `if "driver" in config` but does not check if `config["driver"]` is an empty string. Similarly for other fields. An empty string is truthy in Python, so the check passes but the resulting JSON contains empty values.
**Suggestion:** Add validation for empty strings:
```python
dut_json = {
"make": nic_port.get("vendor") or "Unknown",
"model": nic_port.get("product") or "Unknown",
"hardware version": nic_port.get("version") or "Unknown",
"firmware version": config.get("firmware") or "Unknown",
"deviceBusType": bus_type,
"deviceId": nic_port.get("serial") or "Unknown",
"pmd": config.get("driver") or "Unknown",
"speed": nic_speed or "Unknown",
}
```
This also simplifies the logic by removing the `if "key" in dict else` pattern.
---
### 6. `self._lshw_net_info` accessed without checking it's populated
**Location:** `dts/framework/testbed_model/linux_session.py:214`
```python
port_to_data = {
port.get("businfo"): port for port in self._lshw_net_info if port.get("businfo")
}
```
**Issue:** If `self._lshw_net_info` is not initialized or is empty (due to `lshw` failure earlier in the session), `port_to_data` will be an empty dict and all subsequent lookups will fail with `ConfigurationError("Port ... could not be found")`. This is not wrong per se, but the error message is misleading -- it implies the port doesn't exist when actually the lshw data was never collected.
**Suggestion:** Check that `self._lshw_net_info` is not empty at the start:
```python
if not self._lshw_net_info:
raise ConfigurationError("NIC information is not available. lshw may have failed.")
```
---
## Info
### 1. Code readability - dictionary comprehension could be clearer
**Location:** `dts/framework/testbed_model/linux_session.py:214-216`
```python
port_to_data = {
port.get("businfo"): port for port in self._lshw_net_info if port.get("businfo")
}
```
The conditional `if port.get("businfo")` filters out entries where `businfo` is missing or falsy. This is correct but could be more explicit. Consider extracting to a named variable or adding a comment.
```python
# Build lookup table of bus info to port data, skipping entries without businfo
port_to_data = {
port["businfo"]: port
for port in self._lshw_net_info
if port.get("businfo")
}
```
---
### 2. Consider using `Path` for file operations
**Location:** `dts/framework/test_run.py:354`
```python
with open(f"{SETTINGS.output_dir}/dut_info.json", "w") as file:
```
**Suggestion:** Use `pathlib.Path` for cross-platform path handling:
```python
from pathlib import Path
output_file = Path(SETTINGS.output_dir) / "dut_info.json"
with open(output_file, "w") as file:
json.dump(testrun_nic_info, file, indent=3)
```
---
### 3. JSON formatting - indent parameter
**Location:** `dts/framework/test_run.py:355`
`indent=3` is unusual; Python convention is `indent=4` (matching code indentation) or `indent=2` (compact).
**Suggestion:** Use `indent=4` or `indent=2` for consistency with common JSON formatting.
---
## Summary of Findings
**Errors:** 5 (resource leak risk, missing validation, KeyError risk, inconsistent error handling, type annotation mismatch)
**Warnings:** 6 (documentation, hardcoded strings, naming inconsistency, shell command fragility, empty string validation, missing data check)
**Info:** 3 (readability, path handling, JSON formatting)
The most critical issues are the potential `KeyError` in `port_to_data` access (Error #3) and the missing validation of command output (Error #2), both of which can cause the function to fail with confusing error messages or incorrect data.
More information about the test-report
mailing list