mirror of
https://github.com/esphome/esphome.git
synced 2025-09-13 08:42:18 +01: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.
|
Location of all fixture files.
|
||||||
"""
|
"""
|
||||||
return here / "fixtures"
|
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