[core] esphome clean wipes the whole build directory (#16772)

This commit is contained in:
Jonathan Swoboda
2026-06-03 07:35:23 -04:00
committed by GitHub
parent 997ab11687
commit e4980713d1
7 changed files with 164 additions and 120 deletions

View File

@@ -12,13 +12,6 @@ from esphome.build_gen import platformio
from esphome.core import CORE
@pytest.fixture
def mock_update_storage_json() -> Generator[MagicMock]:
"""Mock update_storage_json for all tests."""
with patch("esphome.build_gen.platformio.update_storage_json") as mock:
yield mock
@pytest.fixture
def mock_write_file_if_changed() -> Generator[MagicMock]:
"""Mock write_file_if_changed for tests."""
@@ -26,9 +19,7 @@ def mock_write_file_if_changed() -> Generator[MagicMock]:
yield mock
def test_write_ini_creates_new_file(
tmp_path: Path, mock_update_storage_json: MagicMock
) -> None:
def test_write_ini_creates_new_file(tmp_path: Path) -> None:
"""Test write_ini creates a new platformio.ini file."""
CORE.build_path = str(tmp_path)
@@ -50,9 +41,7 @@ framework = arduino
assert platformio.INI_AUTO_GENERATE_END in file_content
def test_write_ini_updates_existing_file(
tmp_path: Path, mock_update_storage_json: MagicMock
) -> None:
def test_write_ini_updates_existing_file(tmp_path: Path) -> None:
"""Test write_ini updates existing platformio.ini file."""
CORE.build_path = str(tmp_path)
@@ -97,9 +86,7 @@ framework = arduino
assert "platform = old" not in file_content
def test_write_ini_preserves_custom_sections(
tmp_path: Path, mock_update_storage_json: MagicMock
) -> None:
def test_write_ini_preserves_custom_sections(tmp_path: Path) -> None:
"""Test write_ini preserves custom sections outside auto-generate markers."""
CORE.build_path = str(tmp_path)
@@ -148,7 +135,6 @@ monitor_speed = 115200
def test_write_ini_no_change_when_content_same(
tmp_path: Path,
mock_update_storage_json: MagicMock,
mock_write_file_if_changed: MagicMock,
) -> None:
"""Test write_ini doesn't rewrite file when content is unchanged."""
@@ -174,15 +160,3 @@ def test_write_ini_no_change_when_content_same(
call_args = mock_write_file_if_changed.call_args[0]
assert call_args[0] == ini_file
assert content in call_args[1]
def test_write_ini_calls_update_storage_json(
tmp_path: Path, mock_update_storage_json: MagicMock
) -> None:
"""Test write_ini calls update_storage_json."""
CORE.build_path = str(tmp_path)
content = "[env:test]\nplatform = esp32"
platformio.write_ini(content)
mock_update_storage_json.assert_called_once()

View File

@@ -341,8 +341,8 @@ def test_update_storage_json_logging_when_old_is_none(
with caplog.at_level("INFO"):
update_storage_json()
# Verify clean_build was called
mock_clean_build.assert_called_once()
# Verify clean_build was called with a full wipe (runs before src is written)
mock_clean_build.assert_called_once_with(clear_pio_cache=False, full=True)
# Verify the correct log message was used (not the component removal message)
assert "Core config or version changed, cleaning build files..." in caplog.text
@@ -392,60 +392,50 @@ def test_update_storage_json_logging_components_removed(
new_storage.save.assert_called_once_with("/test/path")
def _mock_cmake_cache_paths(mock_core: MagicMock, tmp_path: Path) -> None:
"""Wire relative_pioenvs_path/relative_build_path to tmp_path subtrees."""
mock_core.name = "test_device"
mock_core.relative_pioenvs_path.side_effect = (tmp_path / ".pioenvs").joinpath
mock_core.relative_build_path.side_effect = tmp_path.joinpath
@patch("esphome.writer.CORE")
def test_clean_cmake_cache(
def test_clean_cmake_cache_platformio(
mock_core: MagicMock,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test clean_cmake_cache removes CMakeCache.txt file."""
# Create directory structure
pioenvs_dir = tmp_path / ".pioenvs"
pioenvs_dir.mkdir()
device_dir = pioenvs_dir / "test_device"
device_dir.mkdir()
cmake_cache_file = device_dir / "CMakeCache.txt"
"""Test clean_cmake_cache removes the PlatformIO CMakeCache.txt."""
_mock_cmake_cache_paths(mock_core, tmp_path)
cmake_cache_file = tmp_path / ".pioenvs" / "test_device" / "CMakeCache.txt"
cmake_cache_file.parent.mkdir(parents=True)
cmake_cache_file.write_text("# CMake cache file")
# Setup mocks
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.name = "test_device"
# Verify file exists before
assert cmake_cache_file.exists()
# Call the function
with caplog.at_level("INFO"):
clean_cmake_cache()
# Verify file was removed
assert not cmake_cache_file.exists()
# Verify logging
assert "Deleting" in caplog.text
assert "CMakeCache.txt" in caplog.text
@patch("esphome.writer.CORE")
def test_clean_cmake_cache_no_pioenvs_dir(
def test_clean_cmake_cache_esp_idf(
mock_core: MagicMock,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test clean_cmake_cache when pioenvs directory doesn't exist."""
# Setup non-existent directory path
pioenvs_dir = tmp_path / ".pioenvs"
"""Test clean_cmake_cache removes the native ESP-IDF build/CMakeCache.txt."""
_mock_cmake_cache_paths(mock_core, tmp_path)
cmake_cache_file = tmp_path / "build" / "CMakeCache.txt"
cmake_cache_file.parent.mkdir(parents=True)
cmake_cache_file.write_text("# CMake cache file")
# Setup mocks
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
with caplog.at_level("INFO"):
clean_cmake_cache()
# Verify directory doesn't exist
assert not pioenvs_dir.exists()
# Call the function - should not crash
clean_cmake_cache()
# Verify directory still doesn't exist
assert not pioenvs_dir.exists()
assert not cmake_cache_file.exists()
assert str(cmake_cache_file) in caplog.text
@patch("esphome.writer.CORE")
@@ -453,27 +443,11 @@ def test_clean_cmake_cache_no_cmake_file(
mock_core: MagicMock,
tmp_path: Path,
) -> None:
"""Test clean_cmake_cache when CMakeCache.txt doesn't exist."""
# Create directory structure without CMakeCache.txt
pioenvs_dir = tmp_path / ".pioenvs"
pioenvs_dir.mkdir()
device_dir = pioenvs_dir / "test_device"
device_dir.mkdir()
cmake_cache_file = device_dir / "CMakeCache.txt"
"""Test clean_cmake_cache when no CMakeCache.txt exists -- should not crash."""
_mock_cmake_cache_paths(mock_core, tmp_path)
# Setup mocks
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.name = "test_device"
# Verify file doesn't exist
assert not cmake_cache_file.exists()
# Call the function - should not crash
clean_cmake_cache()
# Verify file still doesn't exist
assert not cmake_cache_file.exists()
@patch("esphome.writer.CORE")
def test_clean_build(
@@ -507,6 +481,11 @@ def test_clean_build(
managed_components_dir.mkdir()
(managed_components_dir / "espressif__arduino-esp32").mkdir()
# Converted-PIO-library cache (native ESP-IDF), under the data dir.
pio_components_dir = tmp_path / "pio_components"
pio_components_dir.mkdir()
(pio_components_dir / "abc12345").mkdir()
# Create PlatformIO cache directory
platformio_cache_dir = tmp_path / ".platformio" / ".cache"
platformio_cache_dir.mkdir(parents=True)
@@ -529,6 +508,7 @@ def test_clean_build(
assert idedata_cache.exists()
assert idf_build_dir.exists()
assert managed_components_dir.exists()
assert pio_components_dir.exists()
assert platformio_cache_dir.exists()
# Mock PlatformIO's ProjectConfig cache_dir
@@ -554,6 +534,7 @@ def test_clean_build(
assert not idedata_cache.exists()
assert not idf_build_dir.exists()
assert not managed_components_dir.exists()
assert not pio_components_dir.exists()
assert not platformio_cache_dir.exists()
# Verify logging
@@ -567,6 +548,41 @@ def test_clean_build(
assert "PlatformIO cache" in caplog.text
@patch("esphome.writer.CORE")
def test_clean_build_full_wipes_build_dir(
mock_core: MagicMock,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""full=True wipes the whole build dir (incl. src/) but keeps siblings."""
build_dir = tmp_path / "build" / "test"
(build_dir / "src").mkdir(parents=True)
(build_dir / "src" / "main.cpp").write_text("// generated")
(build_dir / "platformio.ini").write_text("[platformio]")
(build_dir / ".pioenvs").mkdir()
idedata_cache = tmp_path / "idedata" / "test.json"
idedata_cache.parent.mkdir()
idedata_cache.write_text("{}")
# A sibling of the build dir (under the data dir) must survive.
survivor = tmp_path / "keep_me.txt"
survivor.write_text("keep")
# build_path may be a str (e.g. set from config); clean_build must coerce.
mock_core.build_path = str(build_dir)
mock_core.name = "test"
mock_core.relative_internal_path.side_effect = tmp_path.joinpath
with caplog.at_level("INFO"):
clean_build(clear_pio_cache=False, full=True)
assert not build_dir.exists()
assert not idedata_cache.exists()
assert survivor.exists()
assert str(build_dir) in caplog.text
@patch("esphome.writer.CORE")
def test_clean_build_partial_exists(
mock_core: MagicMock,
@@ -586,6 +602,7 @@ def test_clean_build_partial_exists(
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir
mock_core.relative_build_path.side_effect = lambda name: tmp_path / name
mock_core.relative_internal_path.side_effect = tmp_path.joinpath
# Verify only pioenvs exists
assert pioenvs_dir.exists()
@@ -623,6 +640,7 @@ def test_clean_build_nothing_exists(
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir
mock_core.relative_build_path.side_effect = lambda name: tmp_path / name
mock_core.relative_internal_path.side_effect = tmp_path.joinpath
# Verify nothing exists
assert not pioenvs_dir.exists()
@@ -659,6 +677,7 @@ def test_clean_build_platformio_not_available(
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir
mock_core.relative_build_path.side_effect = lambda name: tmp_path / name
mock_core.relative_internal_path.side_effect = tmp_path.joinpath
# Verify all exist before
assert pioenvs_dir.exists()
@@ -697,6 +716,7 @@ def test_clean_build_empty_cache_dir(
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.relative_piolibdeps_path.return_value = tmp_path / ".piolibdeps"
mock_core.relative_build_path.side_effect = lambda name: tmp_path / name
mock_core.relative_internal_path.side_effect = tmp_path.joinpath
# Verify pioenvs exists before
assert pioenvs_dir.exists()
@@ -1425,6 +1445,7 @@ def test_clean_build_handles_readonly_files(
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.relative_piolibdeps_path.return_value = tmp_path / ".piolibdeps"
mock_core.relative_build_path.side_effect = lambda name: tmp_path / name
mock_core.relative_internal_path.side_effect = tmp_path.joinpath
# Verify file is read-only
assert not os.access(readonly_file, os.W_OK)
@@ -1489,6 +1510,7 @@ def test_clean_build_reraises_for_other_errors(
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.relative_piolibdeps_path.return_value = tmp_path / ".piolibdeps"
mock_core.relative_build_path.side_effect = lambda name: tmp_path / name
mock_core.relative_internal_path.side_effect = tmp_path.joinpath
try:
# Mock os.access in writer module to return True (writable)