diff --git a/src/mcesptool/components/idf_integration.py b/src/mcesptool/components/idf_integration.py index 3bd0634..eaa5a73 100644 --- a/src/mcesptool/components/idf_integration.py +++ b/src/mcesptool/components/idf_integration.py @@ -133,15 +133,53 @@ def _parse_tools_check(output: str) -> dict[str, Any]: """Parse ``idf_tools.py check`` output. 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] = [] missing: list[str] = [] + current_tool: str | None = None + has_installed_version = False + for line in output.splitlines(): stripped = line.strip() if not stripped: continue - # Lines look like: "xtensa-esp-elf 14.2.0_20241119: found" or "... not found" + + # Multi-line format: "Checking tool " + 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: tool_name = stripped.split(":")[0].strip() installed.append(tool_name) @@ -149,6 +187,10 @@ def _parse_tools_check(output: str) -> dict[str, Any]: tool_name = stripped.split(":")[0].strip() 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} diff --git a/tests/test_idf_integration.py b/tests/test_idf_integration.py index 3f437ee..c9572a4 100644 --- a/tests/test_idf_integration.py +++ b/tests/test_idf_integration.py @@ -170,7 +170,8 @@ class TestParseToolsList: class TestParseToolsCheck: """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 riscv32-esp-elf 14.2.0_20241119: found xtensa-esp-elf-gdb 14.2_20240403: found @@ -179,22 +180,42 @@ class TestParseToolsCheck: 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): - 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 "riscv32-esp-elf 14.2.0_20241119" in result["installed"] 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 "ninja 1.11.1" in result["missing"] 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 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 def test_empty_output(self): @@ -213,6 +234,34 @@ class TestParseToolsCheck: assert len(result["installed"]) == 0 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: """Tests for _parse_export_vars parser."""