mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Add additional coverage for util and writer (#10683)
This commit is contained in:
		| @@ -141,3 +141,170 @@ def test_list_yaml_files_mixed_extensions(tmp_path: Path) -> None: | |||||||
|         str(yaml_file), |         str(yaml_file), | ||||||
|         str(yml_file), |         str(yml_file), | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_list_yaml_files_does_not_recurse_into_subdirectories(tmp_path: Path) -> None: | ||||||
|  |     """Test that list_yaml_files only finds files in specified directory, not subdirectories.""" | ||||||
|  |     # Create directory structure with YAML files at different depths | ||||||
|  |     root = tmp_path / "configs" | ||||||
|  |     root.mkdir() | ||||||
|  |  | ||||||
|  |     # Create YAML files in the root directory | ||||||
|  |     (root / "config1.yaml").write_text("test: 1") | ||||||
|  |     (root / "config2.yml").write_text("test: 2") | ||||||
|  |     (root / "device.yaml").write_text("test: device") | ||||||
|  |  | ||||||
|  |     # Create subdirectory with YAML files (should NOT be found) | ||||||
|  |     subdir = root / "subdir" | ||||||
|  |     subdir.mkdir() | ||||||
|  |     (subdir / "nested1.yaml").write_text("test: nested1") | ||||||
|  |     (subdir / "nested2.yml").write_text("test: nested2") | ||||||
|  |  | ||||||
|  |     # Create deeper subdirectory (should NOT be found) | ||||||
|  |     deep_subdir = subdir / "deeper" | ||||||
|  |     deep_subdir.mkdir() | ||||||
|  |     (deep_subdir / "very_nested.yaml").write_text("test: very_nested") | ||||||
|  |  | ||||||
|  |     # Test listing files from the root directory | ||||||
|  |     result = util.list_yaml_files([str(root)]) | ||||||
|  |  | ||||||
|  |     # Should only find the 3 files in root, not the 3 in subdirectories | ||||||
|  |     assert len(result) == 3 | ||||||
|  |  | ||||||
|  |     # Check that only root-level files are found | ||||||
|  |     assert str(root / "config1.yaml") in result | ||||||
|  |     assert str(root / "config2.yml") in result | ||||||
|  |     assert str(root / "device.yaml") in result | ||||||
|  |  | ||||||
|  |     # Ensure nested files are NOT found | ||||||
|  |     for r in result: | ||||||
|  |         assert "subdir" not in r | ||||||
|  |         assert "deeper" not in r | ||||||
|  |         assert "nested1.yaml" not in r | ||||||
|  |         assert "nested2.yml" not in r | ||||||
|  |         assert "very_nested.yaml" not in r | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_list_yaml_files_excludes_secrets(tmp_path: Path) -> None: | ||||||
|  |     """Test that secrets.yaml and secrets.yml are excluded.""" | ||||||
|  |     root = tmp_path / "configs" | ||||||
|  |     root.mkdir() | ||||||
|  |  | ||||||
|  |     # Create various YAML files including secrets | ||||||
|  |     (root / "config.yaml").write_text("test: config") | ||||||
|  |     (root / "secrets.yaml").write_text("wifi_password: secret123") | ||||||
|  |     (root / "secrets.yml").write_text("api_key: secret456") | ||||||
|  |     (root / "device.yaml").write_text("test: device") | ||||||
|  |  | ||||||
|  |     result = util.list_yaml_files([str(root)]) | ||||||
|  |  | ||||||
|  |     # Should find 2 files (config.yaml and device.yaml), not secrets | ||||||
|  |     assert len(result) == 2 | ||||||
|  |     assert str(root / "config.yaml") in result | ||||||
|  |     assert str(root / "device.yaml") in result | ||||||
|  |     assert str(root / "secrets.yaml") not in result | ||||||
|  |     assert str(root / "secrets.yml") not in result | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_list_yaml_files_excludes_hidden_files(tmp_path: Path) -> None: | ||||||
|  |     """Test that hidden files (starting with .) are excluded.""" | ||||||
|  |     root = tmp_path / "configs" | ||||||
|  |     root.mkdir() | ||||||
|  |  | ||||||
|  |     # Create regular and hidden YAML files | ||||||
|  |     (root / "config.yaml").write_text("test: config") | ||||||
|  |     (root / ".hidden.yaml").write_text("test: hidden") | ||||||
|  |     (root / ".backup.yml").write_text("test: backup") | ||||||
|  |     (root / "device.yaml").write_text("test: device") | ||||||
|  |  | ||||||
|  |     result = util.list_yaml_files([str(root)]) | ||||||
|  |  | ||||||
|  |     # Should find only non-hidden files | ||||||
|  |     assert len(result) == 2 | ||||||
|  |     assert str(root / "config.yaml") in result | ||||||
|  |     assert str(root / "device.yaml") in result | ||||||
|  |     assert str(root / ".hidden.yaml") not in result | ||||||
|  |     assert str(root / ".backup.yml") not in result | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_filter_yaml_files_basic() -> None: | ||||||
|  |     """Test filter_yaml_files function.""" | ||||||
|  |     files = [ | ||||||
|  |         "/path/to/config.yaml", | ||||||
|  |         "/path/to/device.yml", | ||||||
|  |         "/path/to/readme.txt", | ||||||
|  |         "/path/to/script.py", | ||||||
|  |         "/path/to/data.json", | ||||||
|  |         "/path/to/another.yaml", | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     result = util.filter_yaml_files(files) | ||||||
|  |  | ||||||
|  |     assert len(result) == 3 | ||||||
|  |     assert "/path/to/config.yaml" in result | ||||||
|  |     assert "/path/to/device.yml" in result | ||||||
|  |     assert "/path/to/another.yaml" in result | ||||||
|  |     assert "/path/to/readme.txt" not in result | ||||||
|  |     assert "/path/to/script.py" not in result | ||||||
|  |     assert "/path/to/data.json" not in result | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_filter_yaml_files_excludes_secrets() -> None: | ||||||
|  |     """Test that filter_yaml_files excludes secrets files.""" | ||||||
|  |     files = [ | ||||||
|  |         "/path/to/config.yaml", | ||||||
|  |         "/path/to/secrets.yaml", | ||||||
|  |         "/path/to/secrets.yml", | ||||||
|  |         "/path/to/device.yaml", | ||||||
|  |         "/some/dir/secrets.yaml", | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     result = util.filter_yaml_files(files) | ||||||
|  |  | ||||||
|  |     assert len(result) == 2 | ||||||
|  |     assert "/path/to/config.yaml" in result | ||||||
|  |     assert "/path/to/device.yaml" in result | ||||||
|  |     assert "/path/to/secrets.yaml" not in result | ||||||
|  |     assert "/path/to/secrets.yml" not in result | ||||||
|  |     assert "/some/dir/secrets.yaml" not in result | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_filter_yaml_files_excludes_hidden() -> None: | ||||||
|  |     """Test that filter_yaml_files excludes hidden files.""" | ||||||
|  |     files = [ | ||||||
|  |         "/path/to/config.yaml", | ||||||
|  |         "/path/to/.hidden.yaml", | ||||||
|  |         "/path/to/.backup.yml", | ||||||
|  |         "/path/to/device.yaml", | ||||||
|  |         "/some/dir/.config.yaml", | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     result = util.filter_yaml_files(files) | ||||||
|  |  | ||||||
|  |     assert len(result) == 2 | ||||||
|  |     assert "/path/to/config.yaml" in result | ||||||
|  |     assert "/path/to/device.yaml" in result | ||||||
|  |     assert "/path/to/.hidden.yaml" not in result | ||||||
|  |     assert "/path/to/.backup.yml" not in result | ||||||
|  |     assert "/some/dir/.config.yaml" not in result | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_filter_yaml_files_case_sensitive() -> None: | ||||||
|  |     """Test that filter_yaml_files is case-sensitive for extensions.""" | ||||||
|  |     files = [ | ||||||
|  |         "/path/to/config.yaml", | ||||||
|  |         "/path/to/config.YAML", | ||||||
|  |         "/path/to/config.YML", | ||||||
|  |         "/path/to/config.Yaml", | ||||||
|  |         "/path/to/config.yml", | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     result = util.filter_yaml_files(files) | ||||||
|  |  | ||||||
|  |     # Should only match lowercase .yaml and .yml | ||||||
|  |     assert len(result) == 2 | ||||||
|  |     assert "/path/to/config.yaml" in result | ||||||
|  |     assert "/path/to/config.yml" in result | ||||||
|  |     assert "/path/to/config.YAML" not in result | ||||||
|  |     assert "/path/to/config.YML" not in result | ||||||
|  |     assert "/path/to/config.Yaml" not in result | ||||||
|   | |||||||
| @@ -1,13 +1,34 @@ | |||||||
| """Test writer module functionality.""" | """Test writer module functionality.""" | ||||||
|  |  | ||||||
| from collections.abc import Callable | from collections.abc import Callable | ||||||
|  | from pathlib import Path | ||||||
| from typing import Any | from typing import Any | ||||||
| from unittest.mock import MagicMock, patch | from unittest.mock import MagicMock, patch | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
|  | from esphome.core import EsphomeError | ||||||
| from esphome.storage_json import StorageJSON | from esphome.storage_json import StorageJSON | ||||||
| from esphome.writer import storage_should_clean, update_storage_json | 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 | @pytest.fixture | ||||||
| @@ -218,3 +239,396 @@ def test_update_storage_json_logging_components_removed( | |||||||
|  |  | ||||||
|     # Verify save was called |     # Verify save was called | ||||||
|     new_storage.save.assert_called_once_with("/test/path") |     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") | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user