diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index d59523a74a..4d795ea5d9 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -107,9 +107,24 @@ FILTER_PLATFORMIO_LINES = [ r"Warning: DEPRECATED: 'esptool.py' is deprecated. Please use 'esptool' instead. The '.py' suffix will be removed in a future major release.", r"Warning: esp-idf-size exited with code 2", r"esp_idf_size: error: unrecognized arguments: --ng", + r"Package configuration completed successfully", ] +class PlatformioLogFilter(logging.Filter): + """Filter to suppress noisy platformio log messages.""" + + _PATTERN = re.compile( + r"|".join(r"(?:" + pattern + r")" for pattern in FILTER_PLATFORMIO_LINES) + ) + + def filter(self, record: logging.LogRecord) -> bool: + # Only filter messages from platformio-related loggers + if "platformio" not in record.name.lower(): + return True + return self._PATTERN.match(record.getMessage()) is None + + def run_platformio_cli(*args, **kwargs) -> str | int: os.environ["PLATFORMIO_FORCE_COLOR"] = "true" os.environ["PLATFORMIO_BUILD_DIR"] = str(CORE.relative_pioenvs_path().absolute()) @@ -130,7 +145,18 @@ def run_platformio_cli(*args, **kwargs) -> str | int: patch_structhash() patch_file_downloader() - return run_external_command(platformio.__main__.main, *cmd, **kwargs) + + # Add log filter to suppress noisy platformio messages + log_filter = PlatformioLogFilter() if not CORE.verbose else None + if log_filter: + for handler in logging.getLogger().handlers: + handler.addFilter(log_filter) + try: + return run_external_command(platformio.__main__.main, *cmd, **kwargs) + finally: + if log_filter: + for handler in logging.getLogger().handlers: + handler.removeFilter(log_filter) def run_platformio_cli_run(config, verbose, *args, **kwargs) -> str | int: diff --git a/tests/unit_tests/test_platformio_api.py b/tests/unit_tests/test_platformio_api.py index 13ef3516e4..4d7b635e59 100644 --- a/tests/unit_tests/test_platformio_api.py +++ b/tests/unit_tests/test_platformio_api.py @@ -1,6 +1,7 @@ """Tests for platformio_api.py path functions.""" import json +import logging import os from pathlib import Path import shutil @@ -670,3 +671,100 @@ def test_process_stacktrace_bad_alloc( assert "Memory allocation of 512 bytes failed at 40201234" in caplog.text mock_decode_pc.assert_called_once_with(config, "40201234") assert state is False + + +def test_platformio_log_filter_allows_non_platformio_messages() -> None: + """Test that non-platformio logger messages are allowed through.""" + log_filter = platformio_api.PlatformioLogFilter() + record = logging.LogRecord( + name="esphome.core", + level=logging.INFO, + pathname="", + lineno=0, + msg="Some esphome message", + args=(), + exc_info=None, + ) + assert log_filter.filter(record) is True + + +@pytest.mark.parametrize( + "msg", + [ + "Verbose mode can be enabled via `-v, --verbose` option", + "Found 5 compatible libraries", + "Found 123 compatible libraries", + "Building in release mode", + "Building in debug mode", + "Merged 2 ELF section", + "esptool.py v4.7.0", + "esptool v4.8.1", + "PLATFORM: espressif32 @ 6.4.0", + "Using cache: /path/to/cache", + "Package configuration completed successfully", + "Scanning dependencies...", + "Installing dependencies", + "Library Manager: Already installed, built-in library", + "Memory Usage -> https://bit.ly/pio-memory-usage", + ], +) +def test_platformio_log_filter_blocks_noisy_messages(msg: str) -> None: + """Test that noisy platformio messages are filtered out.""" + log_filter = platformio_api.PlatformioLogFilter() + record = logging.LogRecord( + name="platformio.builder", + level=logging.INFO, + pathname="", + lineno=0, + msg=msg, + args=(), + exc_info=None, + ) + assert log_filter.filter(record) is False + + +@pytest.mark.parametrize( + "msg", + [ + "Compiling .pio/build/test/src/main.cpp.o", + "Linking .pio/build/test/firmware.elf", + "Error: something went wrong", + "warning: unused variable", + ], +) +def test_platformio_log_filter_allows_other_platformio_messages(msg: str) -> None: + """Test that non-noisy platformio messages are allowed through.""" + log_filter = platformio_api.PlatformioLogFilter() + record = logging.LogRecord( + name="platformio.builder", + level=logging.INFO, + pathname="", + lineno=0, + msg=msg, + args=(), + exc_info=None, + ) + assert log_filter.filter(record) is True + + +@pytest.mark.parametrize( + "logger_name", + [ + "PLATFORMIO.builder", + "PlatformIO.core", + "platformio.run", + ], +) +def test_platformio_log_filter_case_insensitive_logger_name(logger_name: str) -> None: + """Test that platformio logger name matching is case insensitive.""" + log_filter = platformio_api.PlatformioLogFilter() + record = logging.LogRecord( + name=logger_name, + level=logging.INFO, + pathname="", + lineno=0, + msg="Found 5 compatible libraries", + args=(), + exc_info=None, + ) + assert log_filter.filter(record) is False