diff --git a/esphome/__main__.py b/esphome/__main__.py index ff1769c8f2..55fbbc6c8a 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -984,17 +984,20 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int: # Perform RAM strings analysis _LOGGER.info("Analyzing RAM strings...") - ram_analyzer = RamStringsAnalyzer( - str(firmware_elf), - objdump_path=idedata.objdump_path, - platform=CORE.target_platform, - ) - ram_analyzer.analyze() + try: + ram_analyzer = RamStringsAnalyzer( + str(firmware_elf), + objdump_path=idedata.objdump_path, + platform=CORE.target_platform, + ) + ram_analyzer.analyze() - # Generate and display RAM strings report - ram_report = ram_analyzer.generate_report() - print() - print(ram_report) + # Generate and display RAM strings report + ram_report = ram_analyzer.generate_report() + print() + print(ram_report) + except Exception as e: # pylint: disable=broad-except + _LOGGER.warning("RAM strings analysis failed: %s", e) return 0 diff --git a/esphome/analyze_memory/demangle.py b/esphome/analyze_memory/demangle.py index 7ff06ce870..8999108b51 100644 --- a/esphome/analyze_memory/demangle.py +++ b/esphome/analyze_memory/demangle.py @@ -141,6 +141,16 @@ def batch_demangle( # Process demangled output demangled_lines = result.stdout.strip().split("\n") + + # Check for output length mismatch + if len(demangled_lines) != len(symbols): + _LOGGER.warning( + "c++filt output mismatch: expected %d lines, got %d", + len(symbols), + len(demangled_lines), + ) + return {s: s for s in symbols} + failed_count = 0 for original, stripped, prefix, demangled in zip( diff --git a/esphome/analyze_memory/toolchain.py b/esphome/analyze_memory/toolchain.py index c6cb2bde81..e766252412 100644 --- a/esphome/analyze_memory/toolchain.py +++ b/esphome/analyze_memory/toolchain.py @@ -35,7 +35,10 @@ def find_tool( """ # Try to derive from objdump path first (most reliable) if objdump_path and objdump_path != "objdump": - potential_path = objdump_path.replace("objdump", tool_name) + objdump_file = Path(objdump_path) + # Replace just the filename portion, preserving any prefix (e.g., xtensa-esp32-elf-) + new_name = objdump_file.name.replace("objdump", tool_name) + potential_path = str(objdump_file.with_name(new_name)) if Path(potential_path).exists(): _LOGGER.debug("Found %s at: %s", tool_name, potential_path) return potential_path diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index ccbc5a1306..670d6c16fc 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -269,6 +269,16 @@ def mock_memory_analyzer_cli() -> Generator[Mock]: yield mock_class +@pytest.fixture +def mock_ram_strings_analyzer() -> Generator[Mock]: + """Mock RamStringsAnalyzer for testing.""" + with patch("esphome.analyze_memory.ram_strings.RamStringsAnalyzer") as mock_class: + mock_analyzer = MagicMock() + mock_analyzer.generate_report.return_value = "Mock RAM Strings Report" + mock_class.return_value = mock_analyzer + yield mock_class + + def test_choose_upload_log_host_with_string_default() -> None: """Test with a single string default device.""" setup_core() @@ -2424,6 +2434,7 @@ def test_command_analyze_memory_success( mock_get_idedata: Mock, mock_get_esphome_components: Mock, mock_memory_analyzer_cli: Mock, + mock_ram_strings_analyzer: Mock, ) -> None: """Test command_analyze_memory with successful compilation and analysis.""" setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device") @@ -2471,9 +2482,20 @@ def test_command_analyze_memory_success( mock_analyzer.analyze.assert_called_once() mock_analyzer.generate_report.assert_called_once() - # Verify report was printed + # Verify RAM strings analyzer was created and run + mock_ram_strings_analyzer.assert_called_once_with( + str(firmware_elf), + objdump_path="/path/to/objdump", + platform="esp32", + ) + mock_ram_analyzer = mock_ram_strings_analyzer.return_value + mock_ram_analyzer.analyze.assert_called_once() + mock_ram_analyzer.generate_report.assert_called_once() + + # Verify reports were printed captured = capfd.readouterr() assert "Mock Memory Report" in captured.out + assert "Mock RAM Strings Report" in captured.out def test_command_analyze_memory_with_external_components( @@ -2483,6 +2505,7 @@ def test_command_analyze_memory_with_external_components( mock_get_idedata: Mock, mock_get_esphome_components: Mock, mock_memory_analyzer_cli: Mock, + mock_ram_strings_analyzer: Mock, ) -> None: """Test command_analyze_memory detects external components.""" setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device")