mirror of
https://github.com/esphome/esphome.git
synced 2025-09-14 17:22:20 +01:00
635 lines
20 KiB
Python
635 lines
20 KiB
Python
"""Test writer module functionality."""
|
|
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from esphome.core import EsphomeError
|
|
from esphome.storage_json import StorageJSON
|
|
from esphome.writer import (
|
|
CPP_AUTO_GENERATE_BEGIN,
|
|
CPP_AUTO_GENERATE_END,
|
|
CPP_INCLUDE_BEGIN,
|
|
CPP_INCLUDE_END,
|
|
GITIGNORE_CONTENT,
|
|
clean_build,
|
|
clean_cmake_cache,
|
|
storage_should_clean,
|
|
update_storage_json,
|
|
write_cpp,
|
|
write_gitignore,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_copy_src_tree():
|
|
"""Mock copy_src_tree to avoid side effects during tests."""
|
|
with patch("esphome.writer.copy_src_tree"):
|
|
yield
|
|
|
|
|
|
@pytest.fixture
|
|
def create_storage() -> Callable[..., StorageJSON]:
|
|
"""Factory fixture to create StorageJSON instances."""
|
|
|
|
def _create(
|
|
loaded_integrations: list[str] | None = None, **kwargs: Any
|
|
) -> StorageJSON:
|
|
return StorageJSON(
|
|
storage_version=kwargs.get("storage_version", 1),
|
|
name=kwargs.get("name", "test"),
|
|
friendly_name=kwargs.get("friendly_name", "Test Device"),
|
|
comment=kwargs.get("comment"),
|
|
esphome_version=kwargs.get("esphome_version", "2025.1.0"),
|
|
src_version=kwargs.get("src_version", 1),
|
|
address=kwargs.get("address", "test.local"),
|
|
web_port=kwargs.get("web_port", 80),
|
|
target_platform=kwargs.get("target_platform", "ESP32"),
|
|
build_path=kwargs.get("build_path", "/build"),
|
|
firmware_bin_path=kwargs.get("firmware_bin_path", "/firmware.bin"),
|
|
loaded_integrations=set(loaded_integrations or []),
|
|
loaded_platforms=kwargs.get("loaded_platforms", set()),
|
|
no_mdns=kwargs.get("no_mdns", False),
|
|
framework=kwargs.get("framework", "arduino"),
|
|
core_platform=kwargs.get("core_platform", "esp32"),
|
|
)
|
|
|
|
return _create
|
|
|
|
|
|
def test_storage_should_clean_when_old_is_none(
|
|
create_storage: Callable[..., StorageJSON],
|
|
) -> None:
|
|
"""Test that clean is triggered when old storage is None."""
|
|
new = create_storage(loaded_integrations=["api", "wifi"])
|
|
assert storage_should_clean(None, new) is True
|
|
|
|
|
|
def test_storage_should_clean_when_src_version_changes(
|
|
create_storage: Callable[..., StorageJSON],
|
|
) -> None:
|
|
"""Test that clean is triggered when src_version changes."""
|
|
old = create_storage(loaded_integrations=["api", "wifi"], src_version=1)
|
|
new = create_storage(loaded_integrations=["api", "wifi"], src_version=2)
|
|
assert storage_should_clean(old, new) is True
|
|
|
|
|
|
def test_storage_should_clean_when_build_path_changes(
|
|
create_storage: Callable[..., StorageJSON],
|
|
) -> None:
|
|
"""Test that clean is triggered when build_path changes."""
|
|
old = create_storage(loaded_integrations=["api", "wifi"], build_path="/build1")
|
|
new = create_storage(loaded_integrations=["api", "wifi"], build_path="/build2")
|
|
assert storage_should_clean(old, new) is True
|
|
|
|
|
|
def test_storage_should_clean_when_component_removed(
|
|
create_storage: Callable[..., StorageJSON],
|
|
) -> None:
|
|
"""Test that clean is triggered when a component is removed."""
|
|
old = create_storage(
|
|
loaded_integrations=["api", "wifi", "bluetooth_proxy", "esp32_ble_tracker"]
|
|
)
|
|
new = create_storage(loaded_integrations=["api", "wifi", "esp32_ble_tracker"])
|
|
assert storage_should_clean(old, new) is True
|
|
|
|
|
|
def test_storage_should_clean_when_multiple_components_removed(
|
|
create_storage: Callable[..., StorageJSON],
|
|
) -> None:
|
|
"""Test that clean is triggered when multiple components are removed."""
|
|
old = create_storage(
|
|
loaded_integrations=["api", "wifi", "ota", "web_server", "logger"]
|
|
)
|
|
new = create_storage(loaded_integrations=["api", "wifi", "logger"])
|
|
assert storage_should_clean(old, new) is True
|
|
|
|
|
|
def test_storage_should_not_clean_when_nothing_changes(
|
|
create_storage: Callable[..., StorageJSON],
|
|
) -> None:
|
|
"""Test that clean is not triggered when nothing changes."""
|
|
old = create_storage(loaded_integrations=["api", "wifi", "logger"])
|
|
new = create_storage(loaded_integrations=["api", "wifi", "logger"])
|
|
assert storage_should_clean(old, new) is False
|
|
|
|
|
|
def test_storage_should_not_clean_when_component_added(
|
|
create_storage: Callable[..., StorageJSON],
|
|
) -> None:
|
|
"""Test that clean is not triggered when a component is only added."""
|
|
old = create_storage(loaded_integrations=["api", "wifi"])
|
|
new = create_storage(loaded_integrations=["api", "wifi", "ota"])
|
|
assert storage_should_clean(old, new) is False
|
|
|
|
|
|
def test_storage_should_not_clean_when_other_fields_change(
|
|
create_storage: Callable[..., StorageJSON],
|
|
) -> None:
|
|
"""Test that clean is not triggered when non-relevant fields change."""
|
|
old = create_storage(
|
|
loaded_integrations=["api", "wifi"],
|
|
friendly_name="Old Name",
|
|
esphome_version="2024.12.0",
|
|
)
|
|
new = create_storage(
|
|
loaded_integrations=["api", "wifi"],
|
|
friendly_name="New Name",
|
|
esphome_version="2025.1.0",
|
|
)
|
|
assert storage_should_clean(old, new) is False
|
|
|
|
|
|
def test_storage_edge_case_empty_integrations(
|
|
create_storage: Callable[..., StorageJSON],
|
|
) -> None:
|
|
"""Test edge case when old has integrations but new has none."""
|
|
old = create_storage(loaded_integrations=["api", "wifi"])
|
|
new = create_storage(loaded_integrations=[])
|
|
assert storage_should_clean(old, new) is True
|
|
|
|
|
|
def test_storage_edge_case_from_empty_integrations(
|
|
create_storage: Callable[..., StorageJSON],
|
|
) -> None:
|
|
"""Test edge case when old has no integrations but new has some."""
|
|
old = create_storage(loaded_integrations=[])
|
|
new = create_storage(loaded_integrations=["api", "wifi"])
|
|
assert storage_should_clean(old, new) is False
|
|
|
|
|
|
@patch("esphome.writer.clean_build")
|
|
@patch("esphome.writer.StorageJSON")
|
|
@patch("esphome.writer.storage_path")
|
|
@patch("esphome.writer.CORE")
|
|
def test_update_storage_json_logging_when_old_is_none(
|
|
mock_core: MagicMock,
|
|
mock_storage_path: MagicMock,
|
|
mock_storage_json_class: MagicMock,
|
|
mock_clean_build: MagicMock,
|
|
create_storage: Callable[..., StorageJSON],
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test that update_storage_json doesn't crash when old storage is None.
|
|
|
|
This is a regression test for the AttributeError that occurred when
|
|
old was None and we tried to access old.loaded_integrations.
|
|
"""
|
|
# Setup mocks
|
|
mock_storage_path.return_value = "/test/path"
|
|
mock_storage_json_class.load.return_value = None # Old storage is None
|
|
|
|
new_storage = create_storage(loaded_integrations=["api", "wifi"])
|
|
new_storage.save = MagicMock() # Mock the save method
|
|
mock_storage_json_class.from_esphome_core.return_value = new_storage
|
|
|
|
# Call the function - should not raise AttributeError
|
|
with caplog.at_level("INFO"):
|
|
update_storage_json()
|
|
|
|
# Verify clean_build was called
|
|
mock_clean_build.assert_called_once()
|
|
|
|
# Verify the correct log message was used (not the component removal message)
|
|
assert "Core config or version changed, cleaning build files..." in caplog.text
|
|
assert "Components removed" not in caplog.text
|
|
|
|
# Verify save was called
|
|
new_storage.save.assert_called_once_with("/test/path")
|
|
|
|
|
|
@patch("esphome.writer.clean_build")
|
|
@patch("esphome.writer.StorageJSON")
|
|
@patch("esphome.writer.storage_path")
|
|
@patch("esphome.writer.CORE")
|
|
def test_update_storage_json_logging_components_removed(
|
|
mock_core: MagicMock,
|
|
mock_storage_path: MagicMock,
|
|
mock_storage_json_class: MagicMock,
|
|
mock_clean_build: MagicMock,
|
|
create_storage: Callable[..., StorageJSON],
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test that update_storage_json logs removed components correctly."""
|
|
# Setup mocks
|
|
mock_storage_path.return_value = "/test/path"
|
|
|
|
old_storage = create_storage(loaded_integrations=["api", "wifi", "bluetooth_proxy"])
|
|
new_storage = create_storage(loaded_integrations=["api", "wifi"])
|
|
new_storage.save = MagicMock() # Mock the save method
|
|
|
|
mock_storage_json_class.load.return_value = old_storage
|
|
mock_storage_json_class.from_esphome_core.return_value = new_storage
|
|
|
|
# Call the function
|
|
with caplog.at_level("INFO"):
|
|
update_storage_json()
|
|
|
|
# Verify clean_build was called
|
|
mock_clean_build.assert_called_once()
|
|
|
|
# Verify the correct log message was used with component names
|
|
assert (
|
|
"Components removed (bluetooth_proxy), cleaning build files..." in caplog.text
|
|
)
|
|
assert "Core config or version changed" not in caplog.text
|
|
|
|
# Verify save was called
|
|
new_storage.save.assert_called_once_with("/test/path")
|
|
|
|
|
|
@patch("esphome.writer.CORE")
|
|
def test_clean_cmake_cache(
|
|
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"
|
|
cmake_cache_file.write_text("# CMake cache file")
|
|
|
|
# Setup mocks
|
|
mock_core.relative_pioenvs_path.side_effect = [
|
|
str(pioenvs_dir), # First call for directory check
|
|
str(cmake_cache_file), # Second call for file path
|
|
]
|
|
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(
|
|
mock_core: MagicMock,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Test clean_cmake_cache when pioenvs directory doesn't exist."""
|
|
# Setup non-existent directory path
|
|
pioenvs_dir = tmp_path / ".pioenvs"
|
|
|
|
# Setup mocks
|
|
mock_core.relative_pioenvs_path.return_value = str(pioenvs_dir)
|
|
|
|
# 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()
|
|
|
|
|
|
@patch("esphome.writer.CORE")
|
|
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"
|
|
|
|
# Setup mocks
|
|
mock_core.relative_pioenvs_path.side_effect = [
|
|
str(pioenvs_dir), # First call for directory check
|
|
str(cmake_cache_file), # Second call for file path
|
|
]
|
|
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(
|
|
mock_core: MagicMock,
|
|
tmp_path: Path,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test clean_build removes all build artifacts."""
|
|
# Create directory structure and files
|
|
pioenvs_dir = tmp_path / ".pioenvs"
|
|
pioenvs_dir.mkdir()
|
|
(pioenvs_dir / "test_file.o").write_text("object file")
|
|
|
|
piolibdeps_dir = tmp_path / ".piolibdeps"
|
|
piolibdeps_dir.mkdir()
|
|
(piolibdeps_dir / "library").mkdir()
|
|
|
|
dependencies_lock = tmp_path / "dependencies.lock"
|
|
dependencies_lock.write_text("lock file")
|
|
|
|
# Setup mocks
|
|
mock_core.relative_pioenvs_path.return_value = str(pioenvs_dir)
|
|
mock_core.relative_piolibdeps_path.return_value = str(piolibdeps_dir)
|
|
mock_core.relative_build_path.return_value = str(dependencies_lock)
|
|
|
|
# Verify all exist before
|
|
assert pioenvs_dir.exists()
|
|
assert piolibdeps_dir.exists()
|
|
assert dependencies_lock.exists()
|
|
|
|
# Call the function
|
|
with caplog.at_level("INFO"):
|
|
clean_build()
|
|
|
|
# Verify all were removed
|
|
assert not pioenvs_dir.exists()
|
|
assert not piolibdeps_dir.exists()
|
|
assert not dependencies_lock.exists()
|
|
|
|
# Verify logging
|
|
assert "Deleting" in caplog.text
|
|
assert ".pioenvs" in caplog.text
|
|
assert ".piolibdeps" in caplog.text
|
|
assert "dependencies.lock" in caplog.text
|
|
|
|
|
|
@patch("esphome.writer.CORE")
|
|
def test_clean_build_partial_exists(
|
|
mock_core: MagicMock,
|
|
tmp_path: Path,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test clean_build when only some paths exist."""
|
|
# Create only pioenvs directory
|
|
pioenvs_dir = tmp_path / ".pioenvs"
|
|
pioenvs_dir.mkdir()
|
|
(pioenvs_dir / "test_file.o").write_text("object file")
|
|
|
|
piolibdeps_dir = tmp_path / ".piolibdeps"
|
|
dependencies_lock = tmp_path / "dependencies.lock"
|
|
|
|
# Setup mocks
|
|
mock_core.relative_pioenvs_path.return_value = str(pioenvs_dir)
|
|
mock_core.relative_piolibdeps_path.return_value = str(piolibdeps_dir)
|
|
mock_core.relative_build_path.return_value = str(dependencies_lock)
|
|
|
|
# Verify only pioenvs exists
|
|
assert pioenvs_dir.exists()
|
|
assert not piolibdeps_dir.exists()
|
|
assert not dependencies_lock.exists()
|
|
|
|
# Call the function
|
|
with caplog.at_level("INFO"):
|
|
clean_build()
|
|
|
|
# Verify only existing path was removed
|
|
assert not pioenvs_dir.exists()
|
|
assert not piolibdeps_dir.exists()
|
|
assert not dependencies_lock.exists()
|
|
|
|
# Verify logging - only pioenvs should be logged
|
|
assert "Deleting" in caplog.text
|
|
assert ".pioenvs" in caplog.text
|
|
assert ".piolibdeps" not in caplog.text
|
|
assert "dependencies.lock" not in caplog.text
|
|
|
|
|
|
@patch("esphome.writer.CORE")
|
|
def test_clean_build_nothing_exists(
|
|
mock_core: MagicMock,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Test clean_build when no build artifacts exist."""
|
|
# Setup paths that don't exist
|
|
pioenvs_dir = tmp_path / ".pioenvs"
|
|
piolibdeps_dir = tmp_path / ".piolibdeps"
|
|
dependencies_lock = tmp_path / "dependencies.lock"
|
|
|
|
# Setup mocks
|
|
mock_core.relative_pioenvs_path.return_value = str(pioenvs_dir)
|
|
mock_core.relative_piolibdeps_path.return_value = str(piolibdeps_dir)
|
|
mock_core.relative_build_path.return_value = str(dependencies_lock)
|
|
|
|
# Verify nothing exists
|
|
assert not pioenvs_dir.exists()
|
|
assert not piolibdeps_dir.exists()
|
|
assert not dependencies_lock.exists()
|
|
|
|
# Call the function - should not crash
|
|
clean_build()
|
|
|
|
# Verify nothing was created
|
|
assert not pioenvs_dir.exists()
|
|
assert not piolibdeps_dir.exists()
|
|
assert not dependencies_lock.exists()
|
|
|
|
|
|
@patch("esphome.writer.CORE")
|
|
def test_write_gitignore_creates_new_file(
|
|
mock_core: MagicMock,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Test write_gitignore creates a new .gitignore file when it doesn't exist."""
|
|
gitignore_path = tmp_path / ".gitignore"
|
|
|
|
# Setup mocks
|
|
mock_core.relative_config_path.return_value = str(gitignore_path)
|
|
|
|
# Verify file doesn't exist
|
|
assert not gitignore_path.exists()
|
|
|
|
# Call the function
|
|
write_gitignore()
|
|
|
|
# Verify file was created with correct content
|
|
assert gitignore_path.exists()
|
|
assert gitignore_path.read_text() == GITIGNORE_CONTENT
|
|
|
|
|
|
@patch("esphome.writer.CORE")
|
|
def test_write_gitignore_skips_existing_file(
|
|
mock_core: MagicMock,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Test write_gitignore doesn't overwrite existing .gitignore file."""
|
|
gitignore_path = tmp_path / ".gitignore"
|
|
existing_content = "# Custom gitignore\n/custom_dir/\n"
|
|
gitignore_path.write_text(existing_content)
|
|
|
|
# Setup mocks
|
|
mock_core.relative_config_path.return_value = str(gitignore_path)
|
|
|
|
# Verify file exists with custom content
|
|
assert gitignore_path.exists()
|
|
assert gitignore_path.read_text() == existing_content
|
|
|
|
# Call the function
|
|
write_gitignore()
|
|
|
|
# Verify file was not modified
|
|
assert gitignore_path.exists()
|
|
assert gitignore_path.read_text() == existing_content
|
|
|
|
|
|
@patch("esphome.writer.write_file_if_changed") # Mock to capture output
|
|
@patch("esphome.writer.copy_src_tree") # Keep this mock as it's complex
|
|
@patch("esphome.writer.CORE")
|
|
def test_write_cpp_with_existing_file(
|
|
mock_core: MagicMock,
|
|
mock_copy_src_tree: MagicMock,
|
|
mock_write_file: MagicMock,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Test write_cpp when main.cpp already exists."""
|
|
# Create a real file with markers
|
|
main_cpp = tmp_path / "main.cpp"
|
|
existing_content = f"""#include "esphome.h"
|
|
{CPP_INCLUDE_BEGIN}
|
|
// Old includes
|
|
{CPP_INCLUDE_END}
|
|
void setup() {{
|
|
{CPP_AUTO_GENERATE_BEGIN}
|
|
// Old code
|
|
{CPP_AUTO_GENERATE_END}
|
|
}}
|
|
void loop() {{}}"""
|
|
main_cpp.write_text(existing_content)
|
|
|
|
# Setup mocks
|
|
mock_core.relative_src_path.return_value = str(main_cpp)
|
|
mock_core.cpp_global_section = "// Global section"
|
|
|
|
# Call the function
|
|
test_code = " // New generated code"
|
|
write_cpp(test_code)
|
|
|
|
# Verify copy_src_tree was called
|
|
mock_copy_src_tree.assert_called_once()
|
|
|
|
# Get the content that would be written
|
|
mock_write_file.assert_called_once()
|
|
written_path, written_content = mock_write_file.call_args[0]
|
|
|
|
# Check that markers are preserved and content is updated
|
|
assert CPP_INCLUDE_BEGIN in written_content
|
|
assert CPP_INCLUDE_END in written_content
|
|
assert CPP_AUTO_GENERATE_BEGIN in written_content
|
|
assert CPP_AUTO_GENERATE_END in written_content
|
|
assert test_code in written_content
|
|
assert "// Global section" in written_content
|
|
|
|
|
|
@patch("esphome.writer.write_file_if_changed") # Mock to capture output
|
|
@patch("esphome.writer.copy_src_tree") # Keep this mock as it's complex
|
|
@patch("esphome.writer.CORE")
|
|
def test_write_cpp_creates_new_file(
|
|
mock_core: MagicMock,
|
|
mock_copy_src_tree: MagicMock,
|
|
mock_write_file: MagicMock,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Test write_cpp when main.cpp doesn't exist."""
|
|
# Setup path for new file
|
|
main_cpp = tmp_path / "main.cpp"
|
|
|
|
# Setup mocks
|
|
mock_core.relative_src_path.return_value = str(main_cpp)
|
|
mock_core.cpp_global_section = "// Global section"
|
|
|
|
# Verify file doesn't exist
|
|
assert not main_cpp.exists()
|
|
|
|
# Call the function
|
|
test_code = " // Generated code"
|
|
write_cpp(test_code)
|
|
|
|
# Verify copy_src_tree was called
|
|
mock_copy_src_tree.assert_called_once()
|
|
|
|
# Get the content that would be written
|
|
mock_write_file.assert_called_once()
|
|
written_path, written_content = mock_write_file.call_args[0]
|
|
assert written_path == str(main_cpp)
|
|
|
|
# Check that all necessary parts are in the new file
|
|
assert '#include "esphome.h"' in written_content
|
|
assert CPP_INCLUDE_BEGIN in written_content
|
|
assert CPP_INCLUDE_END in written_content
|
|
assert CPP_AUTO_GENERATE_BEGIN in written_content
|
|
assert CPP_AUTO_GENERATE_END in written_content
|
|
assert test_code in written_content
|
|
assert "void setup()" in written_content
|
|
assert "void loop()" in written_content
|
|
assert "App.setup();" in written_content
|
|
assert "App.loop();" in written_content
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_copy_src_tree")
|
|
@patch("esphome.writer.CORE")
|
|
def test_write_cpp_with_missing_end_marker(
|
|
mock_core: MagicMock,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Test write_cpp raises error when end marker is missing."""
|
|
# Create a file with begin marker but no end marker
|
|
main_cpp = tmp_path / "main.cpp"
|
|
existing_content = f"""#include "esphome.h"
|
|
{CPP_AUTO_GENERATE_BEGIN}
|
|
// Code without end marker"""
|
|
main_cpp.write_text(existing_content)
|
|
|
|
# Setup mocks
|
|
mock_core.relative_src_path.return_value = str(main_cpp)
|
|
|
|
# Call should raise an error
|
|
with pytest.raises(EsphomeError, match="Could not find auto generated code end"):
|
|
write_cpp("// New code")
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_copy_src_tree")
|
|
@patch("esphome.writer.CORE")
|
|
def test_write_cpp_with_duplicate_markers(
|
|
mock_core: MagicMock,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Test write_cpp raises error when duplicate markers exist."""
|
|
# Create a file with duplicate begin markers
|
|
main_cpp = tmp_path / "main.cpp"
|
|
existing_content = f"""#include "esphome.h"
|
|
{CPP_AUTO_GENERATE_BEGIN}
|
|
// First section
|
|
{CPP_AUTO_GENERATE_END}
|
|
{CPP_AUTO_GENERATE_BEGIN}
|
|
// Duplicate section
|
|
{CPP_AUTO_GENERATE_END}"""
|
|
main_cpp.write_text(existing_content)
|
|
|
|
# Setup mocks
|
|
mock_core.relative_src_path.return_value = str(main_cpp)
|
|
|
|
# Call should raise an error
|
|
with pytest.raises(EsphomeError, match="Found multiple auto generate code begins"):
|
|
write_cpp("// New code")
|