mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Improve coverage for various core modules (#10663)
This commit is contained in:
		| @@ -36,3 +36,10 @@ def fixture_path() -> Path: | ||||
|     Location of all fixture files. | ||||
|     """ | ||||
|     return here / "fixtures" | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def setup_core(tmp_path: Path) -> Path: | ||||
|     """Set up CORE with test paths.""" | ||||
|     CORE.config_path = str(tmp_path / "test.yaml") | ||||
|     return tmp_path | ||||
|   | ||||
							
								
								
									
										187
									
								
								tests/unit_tests/test_config_validation_paths.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								tests/unit_tests/test_config_validation_paths.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,187 @@ | ||||
| """Tests for config_validation.py path-related functions.""" | ||||
|  | ||||
| from pathlib import Path | ||||
|  | ||||
| import pytest | ||||
| import voluptuous as vol | ||||
|  | ||||
| from esphome import config_validation as cv | ||||
|  | ||||
|  | ||||
| def test_directory_valid_path(setup_core: Path) -> None: | ||||
|     """Test directory validator with valid directory.""" | ||||
|     test_dir = setup_core / "test_directory" | ||||
|     test_dir.mkdir() | ||||
|  | ||||
|     result = cv.directory("test_directory") | ||||
|  | ||||
|     assert result == "test_directory" | ||||
|  | ||||
|  | ||||
| def test_directory_absolute_path(setup_core: Path) -> None: | ||||
|     """Test directory validator with absolute path.""" | ||||
|     test_dir = setup_core / "test_directory" | ||||
|     test_dir.mkdir() | ||||
|  | ||||
|     result = cv.directory(str(test_dir)) | ||||
|  | ||||
|     assert result == str(test_dir) | ||||
|  | ||||
|  | ||||
| def test_directory_nonexistent_path(setup_core: Path) -> None: | ||||
|     """Test directory validator raises error for non-existent directory.""" | ||||
|     with pytest.raises( | ||||
|         vol.Invalid, match="Could not find directory.*nonexistent_directory" | ||||
|     ): | ||||
|         cv.directory("nonexistent_directory") | ||||
|  | ||||
|  | ||||
| def test_directory_file_instead_of_directory(setup_core: Path) -> None: | ||||
|     """Test directory validator raises error when path is a file.""" | ||||
|     test_file = setup_core / "test_file.txt" | ||||
|     test_file.write_text("content") | ||||
|  | ||||
|     with pytest.raises(vol.Invalid, match="is not a directory"): | ||||
|         cv.directory("test_file.txt") | ||||
|  | ||||
|  | ||||
| def test_directory_with_parent_directory(setup_core: Path) -> None: | ||||
|     """Test directory validator with nested directory structure.""" | ||||
|     nested_dir = setup_core / "parent" / "child" / "grandchild" | ||||
|     nested_dir.mkdir(parents=True) | ||||
|  | ||||
|     result = cv.directory("parent/child/grandchild") | ||||
|  | ||||
|     assert result == "parent/child/grandchild" | ||||
|  | ||||
|  | ||||
| def test_file_valid_path(setup_core: Path) -> None: | ||||
|     """Test file_ validator with valid file.""" | ||||
|     test_file = setup_core / "test_file.yaml" | ||||
|     test_file.write_text("test content") | ||||
|  | ||||
|     result = cv.file_("test_file.yaml") | ||||
|  | ||||
|     assert result == "test_file.yaml" | ||||
|  | ||||
|  | ||||
| def test_file_absolute_path(setup_core: Path) -> None: | ||||
|     """Test file_ validator with absolute path.""" | ||||
|     test_file = setup_core / "test_file.yaml" | ||||
|     test_file.write_text("test content") | ||||
|  | ||||
|     result = cv.file_(str(test_file)) | ||||
|  | ||||
|     assert result == str(test_file) | ||||
|  | ||||
|  | ||||
| def test_file_nonexistent_path(setup_core: Path) -> None: | ||||
|     """Test file_ validator raises error for non-existent file.""" | ||||
|     with pytest.raises(vol.Invalid, match="Could not find file.*nonexistent_file.yaml"): | ||||
|         cv.file_("nonexistent_file.yaml") | ||||
|  | ||||
|  | ||||
| def test_file_directory_instead_of_file(setup_core: Path) -> None: | ||||
|     """Test file_ validator raises error when path is a directory.""" | ||||
|     test_dir = setup_core / "test_directory" | ||||
|     test_dir.mkdir() | ||||
|  | ||||
|     with pytest.raises(vol.Invalid, match="is not a file"): | ||||
|         cv.file_("test_directory") | ||||
|  | ||||
|  | ||||
| def test_file_with_parent_directory(setup_core: Path) -> None: | ||||
|     """Test file_ validator with file in nested directory.""" | ||||
|     nested_dir = setup_core / "configs" / "sensors" | ||||
|     nested_dir.mkdir(parents=True) | ||||
|     test_file = nested_dir / "temperature.yaml" | ||||
|     test_file.write_text("sensor config") | ||||
|  | ||||
|     result = cv.file_("configs/sensors/temperature.yaml") | ||||
|  | ||||
|     assert result == "configs/sensors/temperature.yaml" | ||||
|  | ||||
|  | ||||
| def test_directory_handles_trailing_slash(setup_core: Path) -> None: | ||||
|     """Test directory validator handles trailing slashes correctly.""" | ||||
|     test_dir = setup_core / "test_dir" | ||||
|     test_dir.mkdir() | ||||
|  | ||||
|     result = cv.directory("test_dir/") | ||||
|     assert result == "test_dir/" | ||||
|  | ||||
|     result = cv.directory("test_dir") | ||||
|     assert result == "test_dir" | ||||
|  | ||||
|  | ||||
| def test_file_handles_various_extensions(setup_core: Path) -> None: | ||||
|     """Test file_ validator works with different file extensions.""" | ||||
|     yaml_file = setup_core / "config.yaml" | ||||
|     yaml_file.write_text("yaml content") | ||||
|     assert cv.file_("config.yaml") == "config.yaml" | ||||
|  | ||||
|     yml_file = setup_core / "config.yml" | ||||
|     yml_file.write_text("yml content") | ||||
|     assert cv.file_("config.yml") == "config.yml" | ||||
|  | ||||
|     txt_file = setup_core / "readme.txt" | ||||
|     txt_file.write_text("text content") | ||||
|     assert cv.file_("readme.txt") == "readme.txt" | ||||
|  | ||||
|     no_ext_file = setup_core / "LICENSE" | ||||
|     no_ext_file.write_text("license content") | ||||
|     assert cv.file_("LICENSE") == "LICENSE" | ||||
|  | ||||
|  | ||||
| def test_directory_with_symlink(setup_core: Path) -> None: | ||||
|     """Test directory validator follows symlinks.""" | ||||
|     actual_dir = setup_core / "actual_directory" | ||||
|     actual_dir.mkdir() | ||||
|  | ||||
|     symlink_dir = setup_core / "symlink_directory" | ||||
|     symlink_dir.symlink_to(actual_dir) | ||||
|  | ||||
|     result = cv.directory("symlink_directory") | ||||
|     assert result == "symlink_directory" | ||||
|  | ||||
|  | ||||
| def test_file_with_symlink(setup_core: Path) -> None: | ||||
|     """Test file_ validator follows symlinks.""" | ||||
|     actual_file = setup_core / "actual_file.txt" | ||||
|     actual_file.write_text("content") | ||||
|  | ||||
|     symlink_file = setup_core / "symlink_file.txt" | ||||
|     symlink_file.symlink_to(actual_file) | ||||
|  | ||||
|     result = cv.file_("symlink_file.txt") | ||||
|     assert result == "symlink_file.txt" | ||||
|  | ||||
|  | ||||
| def test_directory_error_shows_full_path(setup_core: Path) -> None: | ||||
|     """Test directory validator error message includes full path.""" | ||||
|     with pytest.raises(vol.Invalid, match=".*missing_dir.*full path:.*"): | ||||
|         cv.directory("missing_dir") | ||||
|  | ||||
|  | ||||
| def test_file_error_shows_full_path(setup_core: Path) -> None: | ||||
|     """Test file_ validator error message includes full path.""" | ||||
|     with pytest.raises(vol.Invalid, match=".*missing_file.yaml.*full path:.*"): | ||||
|         cv.file_("missing_file.yaml") | ||||
|  | ||||
|  | ||||
| def test_directory_with_spaces_in_name(setup_core: Path) -> None: | ||||
|     """Test directory validator handles spaces in directory names.""" | ||||
|     dir_with_spaces = setup_core / "my test directory" | ||||
|     dir_with_spaces.mkdir() | ||||
|  | ||||
|     result = cv.directory("my test directory") | ||||
|     assert result == "my test directory" | ||||
|  | ||||
|  | ||||
| def test_file_with_spaces_in_name(setup_core: Path) -> None: | ||||
|     """Test file_ validator handles spaces in file names.""" | ||||
|     file_with_spaces = setup_core / "my test file.yaml" | ||||
|     file_with_spaces.write_text("content") | ||||
|  | ||||
|     result = cv.file_("my test file.yaml") | ||||
|     assert result == "my test file.yaml" | ||||
							
								
								
									
										196
									
								
								tests/unit_tests/test_external_files.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								tests/unit_tests/test_external_files.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,196 @@ | ||||
| """Tests for external_files.py functions.""" | ||||
|  | ||||
| from pathlib import Path | ||||
| import time | ||||
| from unittest.mock import MagicMock, patch | ||||
|  | ||||
| import pytest | ||||
| import requests | ||||
|  | ||||
| from esphome import external_files | ||||
| from esphome.config_validation import Invalid | ||||
| from esphome.core import CORE, TimePeriod | ||||
|  | ||||
|  | ||||
| def test_compute_local_file_dir(setup_core: Path) -> None: | ||||
|     """Test compute_local_file_dir creates and returns correct path.""" | ||||
|     domain = "font" | ||||
|  | ||||
|     result = external_files.compute_local_file_dir(domain) | ||||
|  | ||||
|     assert isinstance(result, Path) | ||||
|     assert result == Path(CORE.data_dir) / domain | ||||
|     assert result.exists() | ||||
|     assert result.is_dir() | ||||
|  | ||||
|  | ||||
| def test_compute_local_file_dir_nested(setup_core: Path) -> None: | ||||
|     """Test compute_local_file_dir works with nested domains.""" | ||||
|     domain = "images/icons" | ||||
|  | ||||
|     result = external_files.compute_local_file_dir(domain) | ||||
|  | ||||
|     assert result == Path(CORE.data_dir) / "images" / "icons" | ||||
|     assert result.exists() | ||||
|     assert result.is_dir() | ||||
|  | ||||
|  | ||||
| def test_is_file_recent_with_recent_file(setup_core: Path) -> None: | ||||
|     """Test is_file_recent returns True for recently created file.""" | ||||
|     test_file = setup_core / "recent.txt" | ||||
|     test_file.write_text("content") | ||||
|  | ||||
|     refresh = TimePeriod(seconds=3600) | ||||
|  | ||||
|     result = external_files.is_file_recent(str(test_file), refresh) | ||||
|  | ||||
|     assert result is True | ||||
|  | ||||
|  | ||||
| def test_is_file_recent_with_old_file(setup_core: Path) -> None: | ||||
|     """Test is_file_recent returns False for old file.""" | ||||
|     test_file = setup_core / "old.txt" | ||||
|     test_file.write_text("content") | ||||
|  | ||||
|     old_time = time.time() - 7200 | ||||
|  | ||||
|     with patch("os.path.getctime", return_value=old_time): | ||||
|         refresh = TimePeriod(seconds=3600) | ||||
|  | ||||
|         result = external_files.is_file_recent(str(test_file), refresh) | ||||
|  | ||||
|         assert result is False | ||||
|  | ||||
|  | ||||
| def test_is_file_recent_nonexistent_file(setup_core: Path) -> None: | ||||
|     """Test is_file_recent returns False for non-existent file.""" | ||||
|     test_file = setup_core / "nonexistent.txt" | ||||
|     refresh = TimePeriod(seconds=3600) | ||||
|  | ||||
|     result = external_files.is_file_recent(str(test_file), refresh) | ||||
|  | ||||
|     assert result is False | ||||
|  | ||||
|  | ||||
| def test_is_file_recent_with_zero_refresh(setup_core: Path) -> None: | ||||
|     """Test is_file_recent with zero refresh period returns False.""" | ||||
|     test_file = setup_core / "test.txt" | ||||
|     test_file.write_text("content") | ||||
|  | ||||
|     # Mock getctime to return a time 10 seconds ago | ||||
|     with patch("os.path.getctime", return_value=time.time() - 10): | ||||
|         refresh = TimePeriod(seconds=0) | ||||
|         result = external_files.is_file_recent(str(test_file), refresh) | ||||
|         assert result is False | ||||
|  | ||||
|  | ||||
| @patch("esphome.external_files.requests.head") | ||||
| def test_has_remote_file_changed_not_modified( | ||||
|     mock_head: MagicMock, setup_core: Path | ||||
| ) -> None: | ||||
|     """Test has_remote_file_changed returns False when file not modified.""" | ||||
|     test_file = setup_core / "cached.txt" | ||||
|     test_file.write_text("cached content") | ||||
|  | ||||
|     mock_response = MagicMock() | ||||
|     mock_response.status_code = 304 | ||||
|     mock_head.return_value = mock_response | ||||
|  | ||||
|     url = "https://example.com/file.txt" | ||||
|     result = external_files.has_remote_file_changed(url, str(test_file)) | ||||
|  | ||||
|     assert result is False | ||||
|     mock_head.assert_called_once() | ||||
|  | ||||
|     call_args = mock_head.call_args | ||||
|     headers = call_args[1]["headers"] | ||||
|     assert external_files.IF_MODIFIED_SINCE in headers | ||||
|     assert external_files.CACHE_CONTROL in headers | ||||
|  | ||||
|  | ||||
| @patch("esphome.external_files.requests.head") | ||||
| def test_has_remote_file_changed_modified( | ||||
|     mock_head: MagicMock, setup_core: Path | ||||
| ) -> None: | ||||
|     """Test has_remote_file_changed returns True when file modified.""" | ||||
|     test_file = setup_core / "cached.txt" | ||||
|     test_file.write_text("cached content") | ||||
|  | ||||
|     mock_response = MagicMock() | ||||
|     mock_response.status_code = 200 | ||||
|     mock_head.return_value = mock_response | ||||
|  | ||||
|     url = "https://example.com/file.txt" | ||||
|     result = external_files.has_remote_file_changed(url, str(test_file)) | ||||
|  | ||||
|     assert result is True | ||||
|  | ||||
|  | ||||
| def test_has_remote_file_changed_no_local_file(setup_core: Path) -> None: | ||||
|     """Test has_remote_file_changed returns True when local file doesn't exist.""" | ||||
|     test_file = setup_core / "nonexistent.txt" | ||||
|  | ||||
|     url = "https://example.com/file.txt" | ||||
|     result = external_files.has_remote_file_changed(url, str(test_file)) | ||||
|  | ||||
|     assert result is True | ||||
|  | ||||
|  | ||||
| @patch("esphome.external_files.requests.head") | ||||
| def test_has_remote_file_changed_network_error( | ||||
|     mock_head: MagicMock, setup_core: Path | ||||
| ) -> None: | ||||
|     """Test has_remote_file_changed handles network errors gracefully.""" | ||||
|     test_file = setup_core / "cached.txt" | ||||
|     test_file.write_text("cached content") | ||||
|  | ||||
|     mock_head.side_effect = requests.exceptions.RequestException("Network error") | ||||
|  | ||||
|     url = "https://example.com/file.txt" | ||||
|  | ||||
|     with pytest.raises(Invalid, match="Could not check if.*Network error"): | ||||
|         external_files.has_remote_file_changed(url, str(test_file)) | ||||
|  | ||||
|  | ||||
| @patch("esphome.external_files.requests.head") | ||||
| def test_has_remote_file_changed_timeout( | ||||
|     mock_head: MagicMock, setup_core: Path | ||||
| ) -> None: | ||||
|     """Test has_remote_file_changed respects timeout.""" | ||||
|     test_file = setup_core / "cached.txt" | ||||
|     test_file.write_text("cached content") | ||||
|  | ||||
|     mock_response = MagicMock() | ||||
|     mock_response.status_code = 304 | ||||
|     mock_head.return_value = mock_response | ||||
|  | ||||
|     url = "https://example.com/file.txt" | ||||
|     external_files.has_remote_file_changed(url, str(test_file)) | ||||
|  | ||||
|     call_args = mock_head.call_args | ||||
|     assert call_args[1]["timeout"] == external_files.NETWORK_TIMEOUT | ||||
|  | ||||
|  | ||||
| def test_compute_local_file_dir_creates_parent_dirs(setup_core: Path) -> None: | ||||
|     """Test compute_local_file_dir creates parent directories.""" | ||||
|     domain = "level1/level2/level3/level4" | ||||
|  | ||||
|     result = external_files.compute_local_file_dir(domain) | ||||
|  | ||||
|     assert result.exists() | ||||
|     assert result.is_dir() | ||||
|     assert result.parent.name == "level3" | ||||
|     assert result.parent.parent.name == "level2" | ||||
|     assert result.parent.parent.parent.name == "level1" | ||||
|  | ||||
|  | ||||
| def test_is_file_recent_handles_float_seconds(setup_core: Path) -> None: | ||||
|     """Test is_file_recent works with float seconds in TimePeriod.""" | ||||
|     test_file = setup_core / "test.txt" | ||||
|     test_file.write_text("content") | ||||
|  | ||||
|     refresh = TimePeriod(seconds=3600.5) | ||||
|  | ||||
|     result = external_files.is_file_recent(str(test_file), refresh) | ||||
|  | ||||
|     assert result is True | ||||
							
								
								
									
										129
									
								
								tests/unit_tests/test_platformio_api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								tests/unit_tests/test_platformio_api.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | ||||
| """Tests for platformio_api.py path functions.""" | ||||
|  | ||||
| from pathlib import Path | ||||
| from unittest.mock import patch | ||||
|  | ||||
| from esphome import platformio_api | ||||
| from esphome.core import CORE | ||||
|  | ||||
|  | ||||
| def test_idedata_firmware_elf_path(setup_core: Path) -> None: | ||||
|     """Test IDEData.firmware_elf_path returns correct path.""" | ||||
|     CORE.build_path = str(setup_core / "build" / "test") | ||||
|     CORE.name = "test" | ||||
|     raw_data = {"prog_path": "/path/to/firmware.elf"} | ||||
|     idedata = platformio_api.IDEData(raw_data) | ||||
|  | ||||
|     assert idedata.firmware_elf_path == "/path/to/firmware.elf" | ||||
|  | ||||
|  | ||||
| def test_idedata_firmware_bin_path(setup_core: Path) -> None: | ||||
|     """Test IDEData.firmware_bin_path returns Path with .bin extension.""" | ||||
|     CORE.build_path = str(setup_core / "build" / "test") | ||||
|     CORE.name = "test" | ||||
|     prog_path = str(Path("/path/to/firmware.elf")) | ||||
|     raw_data = {"prog_path": prog_path} | ||||
|     idedata = platformio_api.IDEData(raw_data) | ||||
|  | ||||
|     result = idedata.firmware_bin_path | ||||
|     assert isinstance(result, str) | ||||
|     expected = str(Path("/path/to/firmware.bin")) | ||||
|     assert result == expected | ||||
|     assert result.endswith(".bin") | ||||
|  | ||||
|  | ||||
| def test_idedata_firmware_bin_path_preserves_directory(setup_core: Path) -> None: | ||||
|     """Test firmware_bin_path preserves the directory structure.""" | ||||
|     CORE.build_path = str(setup_core / "build" / "test") | ||||
|     CORE.name = "test" | ||||
|     prog_path = str(Path("/complex/path/to/build/firmware.elf")) | ||||
|     raw_data = {"prog_path": prog_path} | ||||
|     idedata = platformio_api.IDEData(raw_data) | ||||
|  | ||||
|     result = idedata.firmware_bin_path | ||||
|     expected = str(Path("/complex/path/to/build/firmware.bin")) | ||||
|     assert result == expected | ||||
|  | ||||
|  | ||||
| def test_idedata_extra_flash_images(setup_core: Path) -> None: | ||||
|     """Test IDEData.extra_flash_images returns list of FlashImage objects.""" | ||||
|     CORE.build_path = str(setup_core / "build" / "test") | ||||
|     CORE.name = "test" | ||||
|     raw_data = { | ||||
|         "prog_path": "/path/to/firmware.elf", | ||||
|         "extra": { | ||||
|             "flash_images": [ | ||||
|                 {"path": "/path/to/bootloader.bin", "offset": "0x1000"}, | ||||
|                 {"path": "/path/to/partition.bin", "offset": "0x8000"}, | ||||
|             ] | ||||
|         }, | ||||
|     } | ||||
|     idedata = platformio_api.IDEData(raw_data) | ||||
|  | ||||
|     images = idedata.extra_flash_images | ||||
|     assert len(images) == 2 | ||||
|     assert all(isinstance(img, platformio_api.FlashImage) for img in images) | ||||
|     assert images[0].path == "/path/to/bootloader.bin" | ||||
|     assert images[0].offset == "0x1000" | ||||
|     assert images[1].path == "/path/to/partition.bin" | ||||
|     assert images[1].offset == "0x8000" | ||||
|  | ||||
|  | ||||
| def test_idedata_extra_flash_images_empty(setup_core: Path) -> None: | ||||
|     """Test extra_flash_images returns empty list when no extra images.""" | ||||
|     CORE.build_path = str(setup_core / "build" / "test") | ||||
|     CORE.name = "test" | ||||
|     raw_data = {"prog_path": "/path/to/firmware.elf", "extra": {"flash_images": []}} | ||||
|     idedata = platformio_api.IDEData(raw_data) | ||||
|  | ||||
|     images = idedata.extra_flash_images | ||||
|     assert images == [] | ||||
|  | ||||
|  | ||||
| def test_idedata_cc_path(setup_core: Path) -> None: | ||||
|     """Test IDEData.cc_path returns compiler path.""" | ||||
|     CORE.build_path = str(setup_core / "build" / "test") | ||||
|     CORE.name = "test" | ||||
|     raw_data = { | ||||
|         "prog_path": "/path/to/firmware.elf", | ||||
|         "cc_path": "/Users/test/.platformio/packages/toolchain-xtensa32/bin/xtensa-esp32-elf-gcc", | ||||
|     } | ||||
|     idedata = platformio_api.IDEData(raw_data) | ||||
|  | ||||
|     assert ( | ||||
|         idedata.cc_path | ||||
|         == "/Users/test/.platformio/packages/toolchain-xtensa32/bin/xtensa-esp32-elf-gcc" | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def test_flash_image_dataclass() -> None: | ||||
|     """Test FlashImage dataclass stores path and offset correctly.""" | ||||
|     image = platformio_api.FlashImage(path="/path/to/image.bin", offset="0x10000") | ||||
|  | ||||
|     assert image.path == "/path/to/image.bin" | ||||
|     assert image.offset == "0x10000" | ||||
|  | ||||
|  | ||||
| def test_load_idedata_returns_dict(setup_core: Path) -> None: | ||||
|     """Test _load_idedata returns parsed idedata dict when successful.""" | ||||
|     CORE.build_path = str(setup_core / "build" / "test") | ||||
|     CORE.name = "test" | ||||
|  | ||||
|     # Create required files | ||||
|     platformio_ini = setup_core / "build" / "test" / "platformio.ini" | ||||
|     platformio_ini.parent.mkdir(parents=True, exist_ok=True) | ||||
|     platformio_ini.touch() | ||||
|  | ||||
|     idedata_path = setup_core / ".esphome" / "idedata" / "test.json" | ||||
|     idedata_path.parent.mkdir(parents=True, exist_ok=True) | ||||
|     idedata_path.write_text('{"prog_path": "/test/firmware.elf"}') | ||||
|  | ||||
|     with patch("esphome.platformio_api.run_platformio_cli_run") as mock_run: | ||||
|         mock_run.return_value = '{"prog_path": "/test/firmware.elf"}' | ||||
|  | ||||
|         config = {"name": "test"} | ||||
|         result = platformio_api._load_idedata(config) | ||||
|  | ||||
|     assert result is not None | ||||
|     assert isinstance(result, dict) | ||||
|     assert result["prog_path"] == "/test/firmware.elf" | ||||
							
								
								
									
										182
									
								
								tests/unit_tests/test_storage_json.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								tests/unit_tests/test_storage_json.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| """Tests for storage_json.py path functions.""" | ||||
|  | ||||
| from pathlib import Path | ||||
| import sys | ||||
| from unittest.mock import patch | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from esphome import storage_json | ||||
| from esphome.core import CORE | ||||
|  | ||||
|  | ||||
| def test_storage_path(setup_core: Path) -> None: | ||||
|     """Test storage_path returns correct path for current config.""" | ||||
|     CORE.config_path = str(setup_core / "my_device.yaml") | ||||
|  | ||||
|     result = storage_json.storage_path() | ||||
|  | ||||
|     data_dir = Path(CORE.data_dir) | ||||
|     expected = str(data_dir / "storage" / "my_device.yaml.json") | ||||
|     assert result == expected | ||||
|  | ||||
|  | ||||
| def test_ext_storage_path(setup_core: Path) -> None: | ||||
|     """Test ext_storage_path returns correct path for given filename.""" | ||||
|     result = storage_json.ext_storage_path("other_device.yaml") | ||||
|  | ||||
|     data_dir = Path(CORE.data_dir) | ||||
|     expected = str(data_dir / "storage" / "other_device.yaml.json") | ||||
|     assert result == expected | ||||
|  | ||||
|  | ||||
| def test_ext_storage_path_handles_various_extensions(setup_core: Path) -> None: | ||||
|     """Test ext_storage_path works with different file extensions.""" | ||||
|     result_yml = storage_json.ext_storage_path("device.yml") | ||||
|     assert result_yml.endswith("device.yml.json") | ||||
|  | ||||
|     result_no_ext = storage_json.ext_storage_path("device") | ||||
|     assert result_no_ext.endswith("device.json") | ||||
|  | ||||
|     result_path = storage_json.ext_storage_path("my/device.yaml") | ||||
|     assert result_path.endswith("device.yaml.json") | ||||
|  | ||||
|  | ||||
| def test_esphome_storage_path(setup_core: Path) -> None: | ||||
|     """Test esphome_storage_path returns correct path.""" | ||||
|     result = storage_json.esphome_storage_path() | ||||
|  | ||||
|     data_dir = Path(CORE.data_dir) | ||||
|     expected = str(data_dir / "esphome.json") | ||||
|     assert result == expected | ||||
|  | ||||
|  | ||||
| def test_ignored_devices_storage_path(setup_core: Path) -> None: | ||||
|     """Test ignored_devices_storage_path returns correct path.""" | ||||
|     result = storage_json.ignored_devices_storage_path() | ||||
|  | ||||
|     data_dir = Path(CORE.data_dir) | ||||
|     expected = str(data_dir / "ignored-devices.json") | ||||
|     assert result == expected | ||||
|  | ||||
|  | ||||
| def test_trash_storage_path(setup_core: Path) -> None: | ||||
|     """Test trash_storage_path returns correct path.""" | ||||
|     CORE.config_path = str(setup_core / "configs" / "device.yaml") | ||||
|  | ||||
|     result = storage_json.trash_storage_path() | ||||
|  | ||||
|     expected = str(setup_core / "configs" / "trash") | ||||
|     assert result == expected | ||||
|  | ||||
|  | ||||
| def test_archive_storage_path(setup_core: Path) -> None: | ||||
|     """Test archive_storage_path returns correct path.""" | ||||
|     CORE.config_path = str(setup_core / "configs" / "device.yaml") | ||||
|  | ||||
|     result = storage_json.archive_storage_path() | ||||
|  | ||||
|     expected = str(setup_core / "configs" / "archive") | ||||
|     assert result == expected | ||||
|  | ||||
|  | ||||
| def test_storage_path_with_subdirectory(setup_core: Path) -> None: | ||||
|     """Test storage paths work correctly when config is in subdirectory.""" | ||||
|     subdir = setup_core / "configs" / "basement" | ||||
|     subdir.mkdir(parents=True, exist_ok=True) | ||||
|     CORE.config_path = str(subdir / "sensor.yaml") | ||||
|  | ||||
|     result = storage_json.storage_path() | ||||
|  | ||||
|     data_dir = Path(CORE.data_dir) | ||||
|     expected = str(data_dir / "storage" / "sensor.yaml.json") | ||||
|     assert result == expected | ||||
|  | ||||
|  | ||||
| def test_storage_json_firmware_bin_path_property(setup_core: Path) -> None: | ||||
|     """Test StorageJSON firmware_bin_path property.""" | ||||
|     storage = storage_json.StorageJSON( | ||||
|         storage_version=1, | ||||
|         name="test_device", | ||||
|         friendly_name="Test Device", | ||||
|         comment=None, | ||||
|         esphome_version="2024.1.0", | ||||
|         src_version=None, | ||||
|         address="192.168.1.100", | ||||
|         web_port=80, | ||||
|         target_platform="ESP32", | ||||
|         build_path="build/test_device", | ||||
|         firmware_bin_path="/path/to/firmware.bin", | ||||
|         loaded_integrations={"wifi", "api"}, | ||||
|         loaded_platforms=set(), | ||||
|         no_mdns=False, | ||||
|     ) | ||||
|  | ||||
|     assert storage.firmware_bin_path == "/path/to/firmware.bin" | ||||
|  | ||||
|  | ||||
| def test_storage_json_save_creates_directory(setup_core: Path, tmp_path: Path) -> None: | ||||
|     """Test StorageJSON.save creates storage directory if it doesn't exist.""" | ||||
|     storage_dir = tmp_path / "new_data" / "storage" | ||||
|     storage_file = storage_dir / "test.json" | ||||
|  | ||||
|     assert not storage_dir.exists() | ||||
|  | ||||
|     storage = storage_json.StorageJSON( | ||||
|         storage_version=1, | ||||
|         name="test", | ||||
|         friendly_name="Test", | ||||
|         comment=None, | ||||
|         esphome_version="2024.1.0", | ||||
|         src_version=None, | ||||
|         address="test.local", | ||||
|         web_port=None, | ||||
|         target_platform="ESP8266", | ||||
|         build_path=None, | ||||
|         firmware_bin_path=None, | ||||
|         loaded_integrations=set(), | ||||
|         loaded_platforms=set(), | ||||
|         no_mdns=False, | ||||
|     ) | ||||
|  | ||||
|     with patch("esphome.storage_json.write_file_if_changed") as mock_write: | ||||
|         storage.save(str(storage_file)) | ||||
|         mock_write.assert_called_once() | ||||
|         call_args = mock_write.call_args[0] | ||||
|         assert call_args[0] == str(storage_file) | ||||
|  | ||||
|  | ||||
| def test_storage_json_from_wizard(setup_core: Path) -> None: | ||||
|     """Test StorageJSON.from_wizard creates correct storage object.""" | ||||
|     storage = storage_json.StorageJSON.from_wizard( | ||||
|         name="my_device", | ||||
|         friendly_name="My Device", | ||||
|         address="my_device.local", | ||||
|         platform="ESP32", | ||||
|     ) | ||||
|  | ||||
|     assert storage.name == "my_device" | ||||
|     assert storage.friendly_name == "My Device" | ||||
|     assert storage.address == "my_device.local" | ||||
|     assert storage.target_platform == "ESP32" | ||||
|     assert storage.build_path is None | ||||
|     assert storage.firmware_bin_path is None | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif(sys.platform == "win32", reason="HA addons don't run on Windows") | ||||
| @patch("esphome.core.is_ha_addon") | ||||
| def test_storage_paths_with_ha_addon(mock_is_ha_addon: bool, tmp_path: Path) -> None: | ||||
|     """Test storage paths when running as Home Assistant addon.""" | ||||
|     mock_is_ha_addon.return_value = True | ||||
|  | ||||
|     CORE.config_path = str(tmp_path / "test.yaml") | ||||
|  | ||||
|     result = storage_json.storage_path() | ||||
|     # When is_ha_addon is True, CORE.data_dir returns "/data" | ||||
|     # This is the standard mount point for HA addon containers | ||||
|     expected = str(Path("/data") / "storage" / "test.yaml.json") | ||||
|     assert result == expected | ||||
|  | ||||
|     result = storage_json.esphome_storage_path() | ||||
|     expected = str(Path("/data") / "esphome.json") | ||||
|     assert result == expected | ||||
		Reference in New Issue
	
	Block a user