Fix idf_tools_check parser for ESP-IDF v5.x multi-line output

The v5.x check command uses multi-line blocks per tool instead of
single-line "tool version: found" format. Parser now tracks
"Checking tool <name>" headings and "version installed in tools
directory:" lines. Old single-line format still supported.
This commit is contained in:
Ryan Malloy 2026-03-01 23:30:09 -07:00
parent 877fb273d0
commit edc1be4cc3
2 changed files with 97 additions and 6 deletions

View File

@ -133,15 +133,53 @@ def _parse_tools_check(output: str) -> dict[str, Any]:
"""Parse ``idf_tools.py check`` output. """Parse ``idf_tools.py check`` output.
Returns dict with ``installed`` and ``missing`` lists. Returns dict with ``installed`` and ``missing`` lists.
ESP-IDF v5.x uses a multi-line format per tool::
Checking tool xtensa-esp-elf
no version found in PATH
version installed in tools directory: esp-13.2.0_20240530
A tool counts as *installed* when at least one ``version installed``
line appears under its heading. It counts as *missing* when there is
no such line (or only "no version found" lines).
The parser also handles the older single-line format
``tool version: found`` / ``tool version: not found``.
""" """
installed: list[str] = [] installed: list[str] = []
missing: list[str] = [] missing: list[str] = []
current_tool: str | None = None
has_installed_version = False
for line in output.splitlines(): for line in output.splitlines():
stripped = line.strip() stripped = line.strip()
if not stripped: if not stripped:
continue continue
# Lines look like: "xtensa-esp-elf 14.2.0_20241119: found" or "... not found"
# Multi-line format: "Checking tool <name>"
if stripped.startswith("Checking tool "):
# Flush previous tool
if current_tool is not None:
(installed if has_installed_version else missing).append(current_tool)
current_tool = stripped[len("Checking tool "):]
has_installed_version = False
continue
# Multi-line format: indented status lines under a tool heading
if current_tool is not None and line[0] in (" ", "\t"):
if "version installed in tools directory:" in stripped:
version = stripped.split(":", 1)[1].strip()
has_installed_version = True
# Enrich the tool name with the installed version
current_tool_with_ver = f"{current_tool} {version}"
# Replace plain name if this is the first version found
if has_installed_version and " " not in current_tool:
current_tool = current_tool_with_ver
continue
# Single-line format: "xtensa-esp-elf 14.2.0: found" (older IDF)
if ": found" in stripped: if ": found" in stripped:
tool_name = stripped.split(":")[0].strip() tool_name = stripped.split(":")[0].strip()
installed.append(tool_name) installed.append(tool_name)
@ -149,6 +187,10 @@ def _parse_tools_check(output: str) -> dict[str, Any]:
tool_name = stripped.split(":")[0].strip() tool_name = stripped.split(":")[0].strip()
missing.append(tool_name) missing.append(tool_name)
# Flush final tool from multi-line parsing
if current_tool is not None:
(installed if has_installed_version else missing).append(current_tool)
return {"installed": installed, "missing": missing} return {"installed": installed, "missing": missing}

View File

@ -170,7 +170,8 @@ class TestParseToolsList:
class TestParseToolsCheck: class TestParseToolsCheck:
"""Tests for _parse_tools_check parser.""" """Tests for _parse_tools_check parser."""
SAMPLE_OUTPUT = textwrap.dedent("""\ # Old single-line format (kept for backwards compat)
SAMPLE_OUTPUT_LEGACY = textwrap.dedent("""\
xtensa-esp-elf 14.2.0_20241119: found xtensa-esp-elf 14.2.0_20241119: found
riscv32-esp-elf 14.2.0_20241119: found riscv32-esp-elf 14.2.0_20241119: found
xtensa-esp-elf-gdb 14.2_20240403: found xtensa-esp-elf-gdb 14.2_20240403: found
@ -179,22 +180,42 @@ class TestParseToolsCheck:
ninja 1.11.1: not found ninja 1.11.1: not found
""") """)
# Real ESP-IDF v5.3 multi-line format
SAMPLE_OUTPUT_V5 = textwrap.dedent("""\
Checking for installed tools...
Checking tool xtensa-esp-elf-gdb
no version found in PATH
version installed in tools directory: 14.2_20240403
Checking tool riscv32-esp-elf-gdb
no version found in PATH
version installed in tools directory: 14.2_20240403
Checking tool xtensa-esp-elf
no version found in PATH
version installed in tools directory: esp-13.2.0_20240530
Checking tool cmake
version found in PATH: 4.2.2
version installed in tools directory: 3.24.0
Checking tool qemu-riscv32
no version found in PATH
""")
# Legacy single-line tests
def test_installed_tools(self): def test_installed_tools(self):
result = _parse_tools_check(self.SAMPLE_OUTPUT) result = _parse_tools_check(self.SAMPLE_OUTPUT_LEGACY)
assert "xtensa-esp-elf 14.2.0_20241119" in result["installed"] assert "xtensa-esp-elf 14.2.0_20241119" in result["installed"]
assert "riscv32-esp-elf 14.2.0_20241119" in result["installed"] assert "riscv32-esp-elf 14.2.0_20241119" in result["installed"]
def test_missing_tools(self): def test_missing_tools(self):
result = _parse_tools_check(self.SAMPLE_OUTPUT) result = _parse_tools_check(self.SAMPLE_OUTPUT_LEGACY)
assert "cmake 3.24.0" in result["missing"] assert "cmake 3.24.0" in result["missing"]
assert "ninja 1.11.1" in result["missing"] assert "ninja 1.11.1" in result["missing"]
def test_installed_count(self): def test_installed_count(self):
result = _parse_tools_check(self.SAMPLE_OUTPUT) result = _parse_tools_check(self.SAMPLE_OUTPUT_LEGACY)
assert len(result["installed"]) == 4 assert len(result["installed"]) == 4
def test_missing_count(self): def test_missing_count(self):
result = _parse_tools_check(self.SAMPLE_OUTPUT) result = _parse_tools_check(self.SAMPLE_OUTPUT_LEGACY)
assert len(result["missing"]) == 2 assert len(result["missing"]) == 2
def test_empty_output(self): def test_empty_output(self):
@ -213,6 +234,34 @@ class TestParseToolsCheck:
assert len(result["installed"]) == 0 assert len(result["installed"]) == 0
assert len(result["missing"]) == 2 assert len(result["missing"]) == 2
# Multi-line format tests (ESP-IDF v5.x)
def test_v5_installed_tools(self):
result = _parse_tools_check(self.SAMPLE_OUTPUT_V5)
names = [t.split()[0] for t in result["installed"]]
assert "xtensa-esp-elf-gdb" in names
assert "riscv32-esp-elf-gdb" in names
assert "xtensa-esp-elf" in names
assert "cmake" in names
def test_v5_missing_tools(self):
result = _parse_tools_check(self.SAMPLE_OUTPUT_V5)
assert "qemu-riscv32" in result["missing"]
def test_v5_installed_count(self):
result = _parse_tools_check(self.SAMPLE_OUTPUT_V5)
assert len(result["installed"]) == 4
def test_v5_missing_count(self):
result = _parse_tools_check(self.SAMPLE_OUTPUT_V5)
assert len(result["missing"]) == 1
def test_v5_version_included_in_name(self):
result = _parse_tools_check(self.SAMPLE_OUTPUT_V5)
# Installed tools should include version from "version installed" line
installed_str = " ".join(result["installed"])
assert "14.2_20240403" in installed_str
assert "esp-13.2.0_20240530" in installed_str
class TestParseExportVars: class TestParseExportVars:
"""Tests for _parse_export_vars parser.""" """Tests for _parse_export_vars parser."""