1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-30 06:33:51 +00:00

Merge remote-tracking branch 'upstream/dev' into integration

This commit is contained in:
J. Nick Koston
2025-09-19 22:44:16 -06:00
104 changed files with 1620 additions and 1005 deletions

View File

@@ -42,9 +42,9 @@ def config_path(request: pytest.FixtureRequest) -> Generator[None]:
if config_dir.exists():
# Set config_path to a dummy yaml file in the config directory
# This ensures CORE.config_dir points to the config directory
CORE.config_path = str(config_dir / "dummy.yaml")
CORE.config_path = config_dir / "dummy.yaml"
else:
CORE.config_path = str(Path(request.fspath).parent / "dummy.yaml")
CORE.config_path = Path(request.fspath).parent / "dummy.yaml"
yield
CORE.config_path = original_path
@@ -131,7 +131,7 @@ def generate_main() -> Generator[Callable[[str | Path], str]]:
"""Generates the C++ main.cpp from a given yaml file and returns it in string form."""
def generator(path: str | Path) -> str:
CORE.config_path = str(path)
CORE.config_path = Path(path)
CORE.config = read_config({})
generate_cpp_contents(CORE.config)
return CORE.cpp_main_section

View File

@@ -7,7 +7,7 @@ display:
- platform: ssd1306_i2c
id: ssd1306_display
model: SSD1306_128X64
reset_pin: ${reset_pin}
reset_pin: ${display_reset_pin}
pages:
- id: page1
lambda: |-
@@ -16,7 +16,7 @@ display:
touchscreen:
- platform: ektf2232
interrupt_pin: ${interrupt_pin}
rts_pin: ${rts_pin}
reset_pin: ${touch_reset_pin}
display: ssd1306_display
on_touch:
- logger.log:

View File

@@ -1,8 +1,8 @@
substitutions:
scl_pin: GPIO16
sda_pin: GPIO17
reset_pin: GPIO13
display_reset_pin: GPIO13
interrupt_pin: GPIO14
rts_pin: GPIO15
touch_reset_pin: GPIO15
<<: !include common.yaml

View File

@@ -1,8 +1,8 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
reset_pin: GPIO3
display_reset_pin: GPIO3
interrupt_pin: GPIO6
rts_pin: GPIO7
touch_reset_pin: GPIO7
<<: !include common.yaml

View File

@@ -1,8 +1,8 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
reset_pin: GPIO3
display_reset_pin: GPIO3
interrupt_pin: GPIO6
rts_pin: GPIO7
touch_reset_pin: GPIO7
<<: !include common.yaml

View File

@@ -1,8 +1,8 @@
substitutions:
scl_pin: GPIO16
sda_pin: GPIO17
reset_pin: GPIO13
display_reset_pin: GPIO13
interrupt_pin: GPIO14
rts_pin: GPIO15
touch_reset_pin: GPIO15
<<: !include common.yaml

View File

@@ -1,8 +1,8 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
reset_pin: GPIO3
display_reset_pin: GPIO3
interrupt_pin: GPIO12
rts_pin: GPIO13
touch_reset_pin: GPIO13
<<: !include common.yaml

View File

@@ -1,8 +1,8 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
reset_pin: GPIO3
display_reset_pin: GPIO3
interrupt_pin: GPIO6
rts_pin: GPIO7
touch_reset_pin: GPIO7
<<: !include common.yaml

View File

@@ -0,0 +1,42 @@
# Comprehensive ESP8266 test for mdns with multiple network components
# Tests the complete priority chain:
# wifi (60) -> mdns (55) -> ota (54) -> web_server_ota (52)
esphome:
name: mdns-comprehensive-test
esp8266:
board: esp01_1m
logger:
level: DEBUG
wifi:
ssid: MySSID
password: password1
# web_server_base should run at priority 65 (before wifi)
web_server:
port: 80
# mdns should run at priority 55 (after wifi at 60)
mdns:
services:
- service: _http
protocol: _tcp
port: 80
# OTA should run at priority 54 (after mdns)
ota:
- platform: esphome
password: "otapassword"
# Test status LED at priority 80
status_led:
pin:
number: GPIO2
inverted: true
# Include API at priority 40
api:
password: "apipassword"

View File

@@ -0,0 +1,15 @@
wifi:
ssid: MySSID
password: password1
power_save_mode: none
uart:
- id: uart_zwave_proxy
tx_pin: ${tx_pin}
rx_pin: ${rx_pin}
baud_rate: 115200
api:
zwave_proxy:
id: zw_proxy

View File

@@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO17
rx_pin: GPIO16
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO17
rx_pin: GPIO16
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
<<: !include common.yaml

View File

@@ -22,7 +22,7 @@ def create_cache_key() -> tuple[int, int, float, int]:
def setup_core():
"""Set up CORE for testing."""
with tempfile.TemporaryDirectory() as tmpdir:
CORE.config_path = str(Path(tmpdir) / "test.yaml")
CORE.config_path = Path(tmpdir) / "test.yaml"
yield
CORE.reset()
@@ -44,7 +44,7 @@ async def dashboard_entries(mock_settings: MagicMock) -> DashboardEntries:
def test_dashboard_entry_path_initialization() -> None:
"""Test DashboardEntry initializes with path correctly."""
test_path = "/test/config/device.yaml"
test_path = Path("/test/config/device.yaml")
cache_key = create_cache_key()
entry = DashboardEntry(test_path, cache_key)
@@ -59,21 +59,21 @@ def test_dashboard_entry_path_with_absolute_path() -> None:
test_path = Path.cwd() / "absolute" / "path" / "to" / "config.yaml"
cache_key = create_cache_key()
entry = DashboardEntry(str(test_path), cache_key)
entry = DashboardEntry(test_path, cache_key)
assert entry.path == str(test_path)
assert Path(entry.path).is_absolute()
assert entry.path == test_path
assert entry.path.is_absolute()
def test_dashboard_entry_path_with_relative_path() -> None:
"""Test DashboardEntry handles relative paths."""
test_path = "configs/device.yaml"
test_path = Path("configs/device.yaml")
cache_key = create_cache_key()
entry = DashboardEntry(test_path, cache_key)
assert entry.path == test_path
assert not Path(entry.path).is_absolute()
assert not entry.path.is_absolute()
@pytest.mark.asyncio
@@ -81,12 +81,12 @@ async def test_dashboard_entries_get_by_path(
dashboard_entries: DashboardEntries,
) -> None:
"""Test getting entry by path."""
test_path = "/test/config/device.yaml"
test_path = Path("/test/config/device.yaml")
entry = DashboardEntry(test_path, create_cache_key())
dashboard_entries._entries[test_path] = entry
dashboard_entries._entries[str(test_path)] = entry
result = dashboard_entries.get(test_path)
result = dashboard_entries.get(str(test_path))
assert result == entry
@@ -104,12 +104,12 @@ async def test_dashboard_entries_path_normalization(
dashboard_entries: DashboardEntries,
) -> None:
"""Test that paths are handled consistently."""
path1 = "/test/config/device.yaml"
path1 = Path("/test/config/device.yaml")
entry = DashboardEntry(path1, create_cache_key())
dashboard_entries._entries[path1] = entry
dashboard_entries._entries[str(path1)] = entry
result = dashboard_entries.get(path1)
result = dashboard_entries.get(str(path1))
assert result == entry
@@ -118,12 +118,12 @@ async def test_dashboard_entries_path_with_spaces(
dashboard_entries: DashboardEntries,
) -> None:
"""Test handling paths with spaces."""
test_path = "/test/config/my device.yaml"
test_path = Path("/test/config/my device.yaml")
entry = DashboardEntry(test_path, create_cache_key())
dashboard_entries._entries[test_path] = entry
dashboard_entries._entries[str(test_path)] = entry
result = dashboard_entries.get(test_path)
result = dashboard_entries.get(str(test_path))
assert result == entry
assert result.path == test_path
@@ -133,18 +133,18 @@ async def test_dashboard_entries_path_with_special_chars(
dashboard_entries: DashboardEntries,
) -> None:
"""Test handling paths with special characters."""
test_path = "/test/config/device-01_test.yaml"
test_path = Path("/test/config/device-01_test.yaml")
entry = DashboardEntry(test_path, create_cache_key())
dashboard_entries._entries[test_path] = entry
dashboard_entries._entries[str(test_path)] = entry
result = dashboard_entries.get(test_path)
result = dashboard_entries.get(str(test_path))
assert result == entry
def test_dashboard_entries_windows_path() -> None:
"""Test handling Windows-style paths."""
test_path = r"C:\Users\test\esphome\device.yaml"
test_path = Path(r"C:\Users\test\esphome\device.yaml")
cache_key = create_cache_key()
entry = DashboardEntry(test_path, cache_key)
@@ -157,28 +157,28 @@ async def test_dashboard_entries_path_to_cache_key_mapping(
dashboard_entries: DashboardEntries,
) -> None:
"""Test internal entries storage with paths and cache keys."""
path1 = "/test/config/device1.yaml"
path2 = "/test/config/device2.yaml"
path1 = Path("/test/config/device1.yaml")
path2 = Path("/test/config/device2.yaml")
entry1 = DashboardEntry(path1, create_cache_key())
entry2 = DashboardEntry(path2, (1, 1, 1.0, 1))
dashboard_entries._entries[path1] = entry1
dashboard_entries._entries[path2] = entry2
dashboard_entries._entries[str(path1)] = entry1
dashboard_entries._entries[str(path2)] = entry2
assert path1 in dashboard_entries._entries
assert path2 in dashboard_entries._entries
assert dashboard_entries._entries[path1].cache_key == create_cache_key()
assert dashboard_entries._entries[path2].cache_key == (1, 1, 1.0, 1)
assert str(path1) in dashboard_entries._entries
assert str(path2) in dashboard_entries._entries
assert dashboard_entries._entries[str(path1)].cache_key == create_cache_key()
assert dashboard_entries._entries[str(path2)].cache_key == (1, 1, 1.0, 1)
def test_dashboard_entry_path_property() -> None:
"""Test that path property returns expected value."""
test_path = "/test/config/device.yaml"
test_path = Path("/test/config/device.yaml")
entry = DashboardEntry(test_path, create_cache_key())
assert entry.path == test_path
assert isinstance(entry.path, str)
assert isinstance(entry.path, Path)
@pytest.mark.asyncio
@@ -187,14 +187,14 @@ async def test_dashboard_entries_all_returns_entries_with_paths(
) -> None:
"""Test that all() returns entries with their paths intact."""
paths = [
"/test/config/device1.yaml",
"/test/config/device2.yaml",
"/test/config/subfolder/device3.yaml",
Path("/test/config/device1.yaml"),
Path("/test/config/device2.yaml"),
Path("/test/config/subfolder/device3.yaml"),
]
for path in paths:
entry = DashboardEntry(path, create_cache_key())
dashboard_entries._entries[path] = entry
dashboard_entries._entries[str(path)] = entry
all_entries = dashboard_entries.async_all()

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
import os
from pathlib import Path
import tempfile
@@ -17,7 +16,7 @@ def dashboard_settings(tmp_path: Path) -> DashboardSettings:
settings = DashboardSettings()
# Resolve symlinks to ensure paths match
resolved_dir = tmp_path.resolve()
settings.config_dir = str(resolved_dir)
settings.config_dir = resolved_dir
settings.absolute_config_dir = resolved_dir
return settings
@@ -26,7 +25,7 @@ def test_rel_path_simple(dashboard_settings: DashboardSettings) -> None:
"""Test rel_path with simple relative path."""
result = dashboard_settings.rel_path("config.yaml")
expected = str(Path(dashboard_settings.config_dir) / "config.yaml")
expected = dashboard_settings.config_dir / "config.yaml"
assert result == expected
@@ -34,9 +33,7 @@ def test_rel_path_multiple_components(dashboard_settings: DashboardSettings) ->
"""Test rel_path with multiple path components."""
result = dashboard_settings.rel_path("subfolder", "device", "config.yaml")
expected = str(
Path(dashboard_settings.config_dir) / "subfolder" / "device" / "config.yaml"
)
expected = dashboard_settings.config_dir / "subfolder" / "device" / "config.yaml"
assert result == expected
@@ -55,7 +52,7 @@ def test_rel_path_absolute_path_within_config(
internal_path.touch()
result = dashboard_settings.rel_path("internal.yaml")
expected = str(Path(dashboard_settings.config_dir) / "internal.yaml")
expected = dashboard_settings.config_dir / "internal.yaml"
assert result == expected
@@ -80,7 +77,7 @@ def test_rel_path_with_pathlib_path(dashboard_settings: DashboardSettings) -> No
path_obj = Path("subfolder") / "config.yaml"
result = dashboard_settings.rel_path(path_obj)
expected = str(Path(dashboard_settings.config_dir) / "subfolder" / "config.yaml")
expected = dashboard_settings.config_dir / "subfolder" / "config.yaml"
assert result == expected
@@ -93,9 +90,7 @@ def test_rel_path_normalizes_slashes(dashboard_settings: DashboardSettings) -> N
assert result1 == result2
# Also test that the result is as expected
expected = os.path.join(
dashboard_settings.config_dir, "folder", "subfolder", "file.yaml"
)
expected = dashboard_settings.config_dir / "folder" / "subfolder" / "file.yaml"
assert result1 == expected
@@ -103,7 +98,7 @@ def test_rel_path_handles_spaces(dashboard_settings: DashboardSettings) -> None:
"""Test rel_path handles paths with spaces."""
result = dashboard_settings.rel_path("my folder", "my config.yaml")
expected = str(Path(dashboard_settings.config_dir) / "my folder" / "my config.yaml")
expected = dashboard_settings.config_dir / "my folder" / "my config.yaml"
assert result == expected
@@ -111,15 +106,13 @@ def test_rel_path_handles_special_chars(dashboard_settings: DashboardSettings) -
"""Test rel_path handles paths with special characters."""
result = dashboard_settings.rel_path("device-01_test", "config.yaml")
expected = str(
Path(dashboard_settings.config_dir) / "device-01_test" / "config.yaml"
)
expected = dashboard_settings.config_dir / "device-01_test" / "config.yaml"
assert result == expected
def test_config_dir_as_path_property(dashboard_settings: DashboardSettings) -> None:
"""Test that config_dir can be accessed and used with Path operations."""
config_path = Path(dashboard_settings.config_dir)
config_path = dashboard_settings.config_dir
assert config_path.exists()
assert config_path.is_dir()
@@ -141,7 +134,7 @@ def test_rel_path_symlink_inside_config(dashboard_settings: DashboardSettings) -
symlink = dashboard_settings.absolute_config_dir / "link.yaml"
symlink.symlink_to(target)
result = dashboard_settings.rel_path("link.yaml")
expected = str(Path(dashboard_settings.config_dir) / "link.yaml")
expected = dashboard_settings.config_dir / "link.yaml"
assert result == expected
@@ -157,12 +150,12 @@ def test_rel_path_symlink_outside_config(dashboard_settings: DashboardSettings)
def test_rel_path_with_none_arg(dashboard_settings: DashboardSettings) -> None:
"""Test rel_path handles None arguments gracefully."""
result = dashboard_settings.rel_path("None")
expected = str(Path(dashboard_settings.config_dir) / "None")
expected = dashboard_settings.config_dir / "None"
assert result == expected
def test_rel_path_with_numeric_args(dashboard_settings: DashboardSettings) -> None:
"""Test rel_path handles numeric arguments."""
result = dashboard_settings.rel_path("123", "456.789")
expected = str(Path(dashboard_settings.config_dir) / "123" / "456.789")
expected = dashboard_settings.config_dir / "123" / "456.789"
assert result == expected

View File

@@ -49,7 +49,7 @@ def mock_trash_storage_path(tmp_path: Path) -> Generator[MagicMock]:
"""Fixture to mock trash_storage_path."""
trash_dir = tmp_path / "trash"
with patch(
"esphome.dashboard.web_server.trash_storage_path", return_value=str(trash_dir)
"esphome.dashboard.web_server.trash_storage_path", return_value=trash_dir
) as mock:
yield mock
@@ -60,7 +60,7 @@ def mock_archive_storage_path(tmp_path: Path) -> Generator[MagicMock]:
archive_dir = tmp_path / "archive"
with patch(
"esphome.dashboard.web_server.archive_storage_path",
return_value=str(archive_dir),
return_value=archive_dir,
) as mock:
yield mock
@@ -257,7 +257,7 @@ async def test_download_binary_handler_with_file(
# Mock storage JSON
mock_storage = Mock()
mock_storage.name = "test_device"
mock_storage.firmware_bin_path = str(firmware_file)
mock_storage.firmware_bin_path = firmware_file
mock_storage_json.load.return_value = mock_storage
response = await dashboard.fetch(
@@ -289,7 +289,7 @@ async def test_download_binary_handler_compressed(
# Mock storage JSON
mock_storage = Mock()
mock_storage.name = "test_device"
mock_storage.firmware_bin_path = str(firmware_file)
mock_storage.firmware_bin_path = firmware_file
mock_storage_json.load.return_value = mock_storage
response = await dashboard.fetch(
@@ -321,7 +321,7 @@ async def test_download_binary_handler_custom_download_name(
# Mock storage JSON
mock_storage = Mock()
mock_storage.name = "test_device"
mock_storage.firmware_bin_path = str(firmware_file)
mock_storage.firmware_bin_path = firmware_file
mock_storage_json.load.return_value = mock_storage
response = await dashboard.fetch(
@@ -355,7 +355,7 @@ async def test_download_binary_handler_idedata_fallback(
# Mock storage JSON
mock_storage = Mock()
mock_storage.name = "test_device"
mock_storage.firmware_bin_path = str(firmware_file)
mock_storage.firmware_bin_path = firmware_file
mock_storage_json.load.return_value = mock_storage
# Mock idedata response
@@ -402,7 +402,7 @@ async def test_edit_request_handler_post_existing(
test_file.write_text("esphome:\n name: original\n")
# Configure the mock settings
mock_dashboard_settings.rel_path.return_value = str(test_file)
mock_dashboard_settings.rel_path.return_value = test_file
mock_dashboard_settings.absolute_config_dir = test_file.parent
new_content = "esphome:\n name: modified\n"
@@ -426,7 +426,7 @@ async def test_unarchive_request_handler(
) -> None:
"""Test the UnArchiveRequestHandler.post method."""
# Set up an archived file
archive_dir = Path(mock_archive_storage_path.return_value)
archive_dir = mock_archive_storage_path.return_value
archive_dir.mkdir(parents=True, exist_ok=True)
archived_file = archive_dir / "archived.yaml"
archived_file.write_text("test content")
@@ -435,7 +435,7 @@ async def test_unarchive_request_handler(
config_dir = tmp_path / "config"
config_dir.mkdir(parents=True, exist_ok=True)
destination_file = config_dir / "archived.yaml"
mock_dashboard_settings.rel_path.return_value = str(destination_file)
mock_dashboard_settings.rel_path.return_value = destination_file
response = await dashboard.fetch(
"/unarchive?configuration=archived.yaml",
@@ -474,7 +474,7 @@ async def test_secret_keys_handler_with_file(
# Configure mock to return our temp secrets file
# Since the file actually exists, os.path.isfile will return True naturally
mock_dashboard_settings.rel_path.return_value = str(secrets_file)
mock_dashboard_settings.rel_path.return_value = secrets_file
response = await dashboard.fetch("/secret_keys", method="GET")
assert response.code == 200
@@ -538,8 +538,8 @@ def test_start_web_server_with_address_port(
) -> None:
"""Test the start_web_server function with address and port."""
app = Mock()
trash_dir = Path(mock_trash_storage_path.return_value)
archive_dir = Path(mock_archive_storage_path.return_value)
trash_dir = mock_trash_storage_path.return_value
archive_dir = mock_archive_storage_path.return_value
# Create trash dir to test migration
trash_dir.mkdir()
@@ -643,12 +643,12 @@ async def test_archive_handler_with_build_folder(
(build_folder / ".pioenvs").mkdir()
mock_dashboard_settings.config_dir = str(config_dir)
mock_dashboard_settings.rel_path.return_value = str(test_config)
mock_archive_storage_path.return_value = str(archive_dir)
mock_dashboard_settings.rel_path.return_value = test_config
mock_archive_storage_path.return_value = archive_dir
mock_storage = MagicMock()
mock_storage.name = "test_device"
mock_storage.build_path = str(build_folder)
mock_storage.build_path = build_folder
mock_storage_json.load.return_value = mock_storage
response = await dashboard.fetch(
@@ -686,8 +686,8 @@ async def test_archive_handler_no_build_folder(
test_config.write_text("esphome:\n name: test_device\n")
mock_dashboard_settings.config_dir = str(config_dir)
mock_dashboard_settings.rel_path.return_value = str(test_config)
mock_archive_storage_path.return_value = str(archive_dir)
mock_dashboard_settings.rel_path.return_value = test_config
mock_archive_storage_path.return_value = archive_dir
mock_storage = MagicMock()
mock_storage.name = "test_device"

View File

@@ -13,14 +13,14 @@ from esphome.dashboard import web_server
def test_get_base_frontend_path_production() -> None:
"""Test get_base_frontend_path in production mode."""
mock_module = MagicMock()
mock_module.where.return_value = "/usr/local/lib/esphome_dashboard"
mock_module.where.return_value = Path("/usr/local/lib/esphome_dashboard")
with (
patch.dict(os.environ, {}, clear=True),
patch.dict("sys.modules", {"esphome_dashboard": mock_module}),
):
result = web_server.get_base_frontend_path()
assert result == "/usr/local/lib/esphome_dashboard"
assert result == Path("/usr/local/lib/esphome_dashboard")
mock_module.where.assert_called_once()
@@ -31,13 +31,12 @@ def test_get_base_frontend_path_dev_mode() -> None:
with patch.dict(os.environ, {"ESPHOME_DASHBOARD_DEV": test_path}):
result = web_server.get_base_frontend_path()
# The function uses os.path.abspath which doesn't resolve symlinks
# We need to match that behavior
# The function uses Path.resolve() which resolves symlinks
# The actual function adds "/" to the path, so we simulate that
test_path_with_slash = test_path if test_path.endswith("/") else test_path + "/"
expected = os.path.abspath(
os.path.join(os.getcwd(), test_path_with_slash, "esphome_dashboard")
)
expected = (
Path(os.getcwd()) / test_path_with_slash / "esphome_dashboard"
).resolve()
assert result == expected
@@ -48,8 +47,8 @@ def test_get_base_frontend_path_dev_mode_with_trailing_slash() -> None:
with patch.dict(os.environ, {"ESPHOME_DASHBOARD_DEV": test_path}):
result = web_server.get_base_frontend_path()
# The function uses os.path.abspath which doesn't resolve symlinks
expected = os.path.abspath(str(Path.cwd() / test_path / "esphome_dashboard"))
# The function uses Path.resolve() which resolves symlinks
expected = (Path.cwd() / test_path / "esphome_dashboard").resolve()
assert result == expected
@@ -60,76 +59,72 @@ def test_get_base_frontend_path_dev_mode_relative_path() -> None:
with patch.dict(os.environ, {"ESPHOME_DASHBOARD_DEV": test_path}):
result = web_server.get_base_frontend_path()
# The function uses os.path.abspath which doesn't resolve symlinks
# We need to match that behavior
# The function uses Path.resolve() which resolves symlinks
# The actual function adds "/" to the path, so we simulate that
test_path_with_slash = test_path if test_path.endswith("/") else test_path + "/"
expected = os.path.abspath(
os.path.join(os.getcwd(), test_path_with_slash, "esphome_dashboard")
)
expected = (
Path(os.getcwd()) / test_path_with_slash / "esphome_dashboard"
).resolve()
assert result == expected
assert Path(result).is_absolute()
assert result.is_absolute()
def test_get_static_path_single_component() -> None:
"""Test get_static_path with single path component."""
with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
mock_base.return_value = "/base/frontend"
mock_base.return_value = Path("/base/frontend")
result = web_server.get_static_path("file.js")
assert result == os.path.join("/base/frontend", "static", "file.js")
assert result == Path("/base/frontend") / "static" / "file.js"
def test_get_static_path_multiple_components() -> None:
"""Test get_static_path with multiple path components."""
with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
mock_base.return_value = "/base/frontend"
mock_base.return_value = Path("/base/frontend")
result = web_server.get_static_path("js", "esphome", "index.js")
assert result == os.path.join(
"/base/frontend", "static", "js", "esphome", "index.js"
assert (
result == Path("/base/frontend") / "static" / "js" / "esphome" / "index.js"
)
def test_get_static_path_empty_args() -> None:
"""Test get_static_path with no arguments."""
with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
mock_base.return_value = "/base/frontend"
mock_base.return_value = Path("/base/frontend")
result = web_server.get_static_path()
assert result == os.path.join("/base/frontend", "static")
assert result == Path("/base/frontend") / "static"
def test_get_static_path_with_pathlib_path() -> None:
"""Test get_static_path with Path objects."""
with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
mock_base.return_value = "/base/frontend"
mock_base.return_value = Path("/base/frontend")
path_obj = Path("js") / "app.js"
result = web_server.get_static_path(str(path_obj))
assert result == os.path.join("/base/frontend", "static", "js", "app.js")
assert result == Path("/base/frontend") / "static" / "js" / "app.js"
def test_get_static_file_url_production() -> None:
"""Test get_static_file_url in production mode."""
web_server.get_static_file_url.cache_clear()
mock_module = MagicMock()
mock_file = MagicMock()
mock_file.read.return_value = b"test content"
mock_file.__enter__ = MagicMock(return_value=mock_file)
mock_file.__exit__ = MagicMock(return_value=None)
mock_path = MagicMock(spec=Path)
mock_path.read_bytes.return_value = b"test content"
with (
patch.dict(os.environ, {}, clear=True),
patch.dict("sys.modules", {"esphome_dashboard": mock_module}),
patch("esphome.dashboard.web_server.get_static_path") as mock_get_path,
patch("esphome.dashboard.web_server.open", create=True, return_value=mock_file),
):
mock_get_path.return_value = "/fake/path/js/app.js"
mock_get_path.return_value = mock_path
result = web_server.get_static_file_url("js/app.js")
assert result.startswith("./static/js/app.js?hash=")
@@ -182,26 +177,26 @@ def test_load_file_compressed_path(tmp_path: Path) -> None:
def test_path_normalization_in_static_path() -> None:
"""Test that paths are normalized correctly."""
with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
mock_base.return_value = "/base/frontend"
mock_base.return_value = Path("/base/frontend")
# Test with separate components
result1 = web_server.get_static_path("js", "app.js")
result2 = web_server.get_static_path("js", "app.js")
assert result1 == result2
assert result1 == os.path.join("/base/frontend", "static", "js", "app.js")
assert result1 == Path("/base/frontend") / "static" / "js" / "app.js"
def test_windows_path_handling() -> None:
"""Test handling of Windows-style paths."""
with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
mock_base.return_value = r"C:\Program Files\esphome\frontend"
mock_base.return_value = Path(r"C:\Program Files\esphome\frontend")
result = web_server.get_static_path("js", "app.js")
# os.path.join should handle this correctly on the platform
expected = os.path.join(
r"C:\Program Files\esphome\frontend", "static", "js", "app.js"
# Path should handle this correctly on the platform
expected = (
Path(r"C:\Program Files\esphome\frontend") / "static" / "js" / "app.js"
)
assert result == expected
@@ -209,22 +204,20 @@ def test_windows_path_handling() -> None:
def test_path_with_special_characters() -> None:
"""Test paths with special characters."""
with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
mock_base.return_value = "/base/frontend"
mock_base.return_value = Path("/base/frontend")
result = web_server.get_static_path("js-modules", "app_v1.0.js")
assert result == os.path.join(
"/base/frontend", "static", "js-modules", "app_v1.0.js"
assert (
result == Path("/base/frontend") / "static" / "js-modules" / "app_v1.0.js"
)
def test_path_with_spaces() -> None:
"""Test paths with spaces."""
with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
mock_base.return_value = "/base/my frontend"
mock_base.return_value = Path("/base/my frontend")
result = web_server.get_static_path("my js", "my app.js")
assert result == os.path.join(
"/base/my frontend", "static", "my js", "my app.js"
)
assert result == Path("/base/my frontend") / "static" / "my js" / "my app.js"

View File

@@ -1,56 +0,0 @@
import os
from pathlib import Path
from unittest.mock import patch
import py
import pytest
from esphome.dashboard.util.file import write_file, write_utf8_file
def test_write_utf8_file(tmp_path: Path) -> None:
write_utf8_file(tmp_path.joinpath("foo.txt"), "foo")
assert tmp_path.joinpath("foo.txt").read_text() == "foo"
with pytest.raises(OSError):
write_utf8_file(Path("/dev/not-writable"), "bar")
def test_write_file(tmp_path: Path) -> None:
write_file(tmp_path.joinpath("foo.txt"), b"foo")
assert tmp_path.joinpath("foo.txt").read_text() == "foo"
def test_write_utf8_file_fails_at_rename(
tmpdir: py.path.local, caplog: pytest.LogCaptureFixture
) -> None:
"""Test that if rename fails not not remove, we do not log the failed cleanup."""
test_dir = tmpdir.mkdir("files")
test_file = Path(test_dir / "test.json")
with (
pytest.raises(OSError),
patch("esphome.dashboard.util.file.os.replace", side_effect=OSError),
):
write_utf8_file(test_file, '{"some":"data"}', False)
assert not os.path.exists(test_file)
assert "File replacement cleanup failed" not in caplog.text
def test_write_utf8_file_fails_at_rename_and_remove(
tmpdir: py.path.local, caplog: pytest.LogCaptureFixture
) -> None:
"""Test that if rename and remove both fail, we log the failed cleanup."""
test_dir = tmpdir.mkdir("files")
test_file = Path(test_dir / "test.json")
with (
pytest.raises(OSError),
patch("esphome.dashboard.util.file.os.remove", side_effect=OSError),
patch("esphome.dashboard.util.file.os.replace", side_effect=OSError),
):
write_utf8_file(test_file, '{"some":"data"}', False)
assert "File replacement cleanup failed" in caplog.text

View File

@@ -271,7 +271,7 @@ async def compile_esphome(
def _read_config_and_get_binary():
CORE.reset() # Reset CORE state between test runs
CORE.config_path = str(config_path)
CORE.config_path = config_path
config = esphome.config.read_config(
{"command": "compile", "config": str(config_path)}
)

View File

@@ -172,7 +172,7 @@ def test_write_ini_no_change_when_content_same(
# write_file_if_changed should be called with the same content
mock_write_file_if_changed.assert_called_once()
call_args = mock_write_file_if_changed.call_args[0]
assert call_args[0] == str(ini_file)
assert call_args[0] == ini_file
assert content in call_args[1]

View File

@@ -43,7 +43,7 @@ def fixture_path() -> Path:
@pytest.fixture
def setup_core(tmp_path: Path) -> Path:
"""Set up CORE with test paths."""
CORE.config_path = str(tmp_path / "test.yaml")
CORE.config_path = tmp_path / "test.yaml"
return tmp_path

View File

@@ -10,7 +10,7 @@ from esphome.core import CORE
def load_config_from_yaml(
yaml_file: Callable[[str], str], yaml_content: str
yaml_file: Callable[[str], Path], yaml_content: str
) -> Config | None:
"""Load configuration from YAML content."""
yaml_path = yaml_file(yaml_content)
@@ -25,7 +25,7 @@ def load_config_from_yaml(
def load_config_from_fixture(
yaml_file: Callable[[str], str], fixture_name: str, fixtures_dir: Path
yaml_file: Callable[[str], Path], fixture_name: str, fixtures_dir: Path
) -> Config | None:
"""Load configuration from a fixture file."""
fixture_path = fixtures_dir / fixture_name

View File

@@ -7,12 +7,12 @@ import pytest
@pytest.fixture
def yaml_file(tmp_path: Path) -> Callable[[str], str]:
def yaml_file(tmp_path: Path) -> Callable[[str], Path]:
"""Create a temporary YAML file for testing."""
def _yaml_file(content: str) -> str:
def _yaml_file(content: str) -> Path:
yaml_path = tmp_path / "test.yaml"
yaml_path.write_text(content)
return str(yaml_path)
return yaml_path
return _yaml_file

View File

@@ -289,7 +289,7 @@ def test_valid_include_with_angle_brackets() -> None:
def test_valid_include_with_valid_file(tmp_path: Path) -> None:
"""Test valid_include accepts valid include files."""
CORE.config_path = str(tmp_path / "test.yaml")
CORE.config_path = tmp_path / "test.yaml"
include_file = tmp_path / "include.h"
include_file.touch()
@@ -298,7 +298,7 @@ def test_valid_include_with_valid_file(tmp_path: Path) -> None:
def test_valid_include_with_valid_directory(tmp_path: Path) -> None:
"""Test valid_include accepts valid directories."""
CORE.config_path = str(tmp_path / "test.yaml")
CORE.config_path = tmp_path / "test.yaml"
include_dir = tmp_path / "includes"
include_dir.mkdir()
@@ -307,7 +307,7 @@ def test_valid_include_with_valid_directory(tmp_path: Path) -> None:
def test_valid_include_invalid_extension(tmp_path: Path) -> None:
"""Test valid_include rejects files with invalid extensions."""
CORE.config_path = str(tmp_path / "test.yaml")
CORE.config_path = tmp_path / "test.yaml"
invalid_file = tmp_path / "file.txt"
invalid_file.touch()
@@ -481,7 +481,7 @@ def test_include_file_header(tmp_path: Path, mock_copy_file_if_changed: Mock) ->
src_file = tmp_path / "source.h"
src_file.write_text("// Header content")
CORE.build_path = str(tmp_path / "build")
CORE.build_path = tmp_path / "build"
with patch("esphome.core.config.cg") as mock_cg:
# Mock RawStatement to capture the text
@@ -494,7 +494,7 @@ def test_include_file_header(tmp_path: Path, mock_copy_file_if_changed: Mock) ->
mock_cg.RawStatement.side_effect = raw_statement_side_effect
config.include_file(str(src_file), "test.h")
config.include_file(src_file, Path("test.h"))
mock_copy_file_if_changed.assert_called_once()
mock_cg.add_global.assert_called_once()
@@ -507,10 +507,10 @@ def test_include_file_cpp(tmp_path: Path, mock_copy_file_if_changed: Mock) -> No
src_file = tmp_path / "source.cpp"
src_file.write_text("// CPP content")
CORE.build_path = str(tmp_path / "build")
CORE.build_path = tmp_path / "build"
with patch("esphome.core.config.cg") as mock_cg:
config.include_file(str(src_file), "test.cpp")
config.include_file(src_file, Path("test.cpp"))
mock_copy_file_if_changed.assert_called_once()
# Should not add include statement for .cpp files
@@ -602,8 +602,8 @@ async def test_add_includes_with_single_file(
mock_cg_with_include_capture: tuple[Mock, list[str]],
) -> None:
"""Test add_includes copies a single header file to build directory."""
CORE.config_path = str(tmp_path / "config.yaml")
CORE.build_path = str(tmp_path / "build")
CORE.config_path = tmp_path / "config.yaml"
CORE.build_path = tmp_path / "build"
os.makedirs(CORE.build_path, exist_ok=True)
# Create include file
@@ -617,7 +617,7 @@ async def test_add_includes_with_single_file(
# Verify copy_file_if_changed was called to copy the file
# Note: add_includes adds files to a src/ subdirectory
mock_copy_file_if_changed.assert_called_once_with(
str(include_file), str(Path(CORE.build_path) / "src" / "my_header.h")
include_file, CORE.build_path / "src" / "my_header.h"
)
# Verify include statement was added
@@ -632,8 +632,8 @@ async def test_add_includes_with_directory_unix(
mock_cg_with_include_capture: tuple[Mock, list[str]],
) -> None:
"""Test add_includes copies all files from a directory on Unix."""
CORE.config_path = str(tmp_path / "config.yaml")
CORE.build_path = str(tmp_path / "build")
CORE.config_path = tmp_path / "config.yaml"
CORE.build_path = tmp_path / "build"
os.makedirs(CORE.build_path, exist_ok=True)
# Create include directory with files
@@ -677,8 +677,8 @@ async def test_add_includes_with_directory_windows(
mock_cg_with_include_capture: tuple[Mock, list[str]],
) -> None:
"""Test add_includes copies all files from a directory on Windows."""
CORE.config_path = str(tmp_path / "config.yaml")
CORE.build_path = str(tmp_path / "build")
CORE.config_path = tmp_path / "config.yaml"
CORE.build_path = tmp_path / "build"
os.makedirs(CORE.build_path, exist_ok=True)
# Create include directory with files
@@ -719,8 +719,8 @@ async def test_add_includes_with_multiple_sources(
tmp_path: Path, mock_copy_file_if_changed: Mock
) -> None:
"""Test add_includes with multiple files and directories."""
CORE.config_path = str(tmp_path / "config.yaml")
CORE.build_path = str(tmp_path / "build")
CORE.config_path = tmp_path / "config.yaml"
CORE.build_path = tmp_path / "build"
os.makedirs(CORE.build_path, exist_ok=True)
# Create various include sources
@@ -747,8 +747,8 @@ async def test_add_includes_empty_directory(
tmp_path: Path, mock_copy_file_if_changed: Mock
) -> None:
"""Test add_includes with an empty directory doesn't fail."""
CORE.config_path = str(tmp_path / "config.yaml")
CORE.build_path = str(tmp_path / "build")
CORE.config_path = tmp_path / "config.yaml"
CORE.build_path = tmp_path / "build"
os.makedirs(CORE.build_path, exist_ok=True)
# Create empty directory
@@ -769,8 +769,8 @@ async def test_add_includes_preserves_directory_structure_unix(
tmp_path: Path, mock_copy_file_if_changed: Mock
) -> None:
"""Test that add_includes preserves relative directory structure on Unix."""
CORE.config_path = str(tmp_path / "config.yaml")
CORE.build_path = str(tmp_path / "build")
CORE.config_path = tmp_path / "config.yaml"
CORE.build_path = tmp_path / "build"
os.makedirs(CORE.build_path, exist_ok=True)
# Create nested directory structure
@@ -793,8 +793,8 @@ async def test_add_includes_preserves_directory_structure_unix(
dest_paths = [call[0][1] for call in calls]
# Check that relative paths are preserved
assert any("lib/src/core.h" in path for path in dest_paths)
assert any("lib/utils/helper.h" in path for path in dest_paths)
assert any("lib/src/core.h" in str(path) for path in dest_paths)
assert any("lib/utils/helper.h" in str(path) for path in dest_paths)
@pytest.mark.asyncio
@@ -803,8 +803,8 @@ async def test_add_includes_preserves_directory_structure_windows(
tmp_path: Path, mock_copy_file_if_changed: Mock
) -> None:
"""Test that add_includes preserves relative directory structure on Windows."""
CORE.config_path = str(tmp_path / "config.yaml")
CORE.build_path = str(tmp_path / "build")
CORE.config_path = tmp_path / "config.yaml"
CORE.build_path = tmp_path / "build"
os.makedirs(CORE.build_path, exist_ok=True)
# Create nested directory structure
@@ -827,8 +827,8 @@ async def test_add_includes_preserves_directory_structure_windows(
dest_paths = [call[0][1] for call in calls]
# Check that relative paths are preserved
assert any("lib\\src\\core.h" in path for path in dest_paths)
assert any("lib\\utils\\helper.h" in path for path in dest_paths)
assert any("lib\\src\\core.h" in str(path) for path in dest_paths)
assert any("lib\\utils\\helper.h" in str(path) for path in dest_paths)
@pytest.mark.asyncio
@@ -836,8 +836,8 @@ async def test_add_includes_overwrites_existing_files(
tmp_path: Path, mock_copy_file_if_changed: Mock
) -> None:
"""Test that add_includes overwrites existing files in build directory."""
CORE.config_path = str(tmp_path / "config.yaml")
CORE.build_path = str(tmp_path / "build")
CORE.config_path = tmp_path / "config.yaml"
CORE.build_path = tmp_path / "build"
os.makedirs(CORE.build_path, exist_ok=True)
# Create include file
@@ -850,5 +850,5 @@ async def test_add_includes_overwrites_existing_files(
# Verify copy_file_if_changed was called (it handles overwriting)
# Note: add_includes adds files to a src/ subdirectory
mock_copy_file_if_changed.assert_called_once_with(
str(include_file), str(Path(CORE.build_path) / "src" / "header.h")
include_file, CORE.build_path / "src" / "header.h"
)

View File

@@ -15,7 +15,7 @@ def test_directory_valid_path(setup_core: Path) -> None:
result = cv.directory("test_directory")
assert result == "test_directory"
assert result == test_dir
def test_directory_absolute_path(setup_core: Path) -> None:
@@ -25,7 +25,7 @@ def test_directory_absolute_path(setup_core: Path) -> None:
result = cv.directory(str(test_dir))
assert result == str(test_dir)
assert result == test_dir
def test_directory_nonexistent_path(setup_core: Path) -> None:
@@ -52,7 +52,7 @@ def test_directory_with_parent_directory(setup_core: Path) -> None:
result = cv.directory("parent/child/grandchild")
assert result == "parent/child/grandchild"
assert result == nested_dir
def test_file_valid_path(setup_core: Path) -> None:
@@ -62,7 +62,7 @@ def test_file_valid_path(setup_core: Path) -> None:
result = cv.file_("test_file.yaml")
assert result == "test_file.yaml"
assert result == test_file
def test_file_absolute_path(setup_core: Path) -> None:
@@ -72,7 +72,7 @@ def test_file_absolute_path(setup_core: Path) -> None:
result = cv.file_(str(test_file))
assert result == str(test_file)
assert result == test_file
def test_file_nonexistent_path(setup_core: Path) -> None:
@@ -99,7 +99,7 @@ def test_file_with_parent_directory(setup_core: Path) -> None:
result = cv.file_("configs/sensors/temperature.yaml")
assert result == "configs/sensors/temperature.yaml"
assert result == test_file
def test_directory_handles_trailing_slash(setup_core: Path) -> None:
@@ -108,29 +108,29 @@ def test_directory_handles_trailing_slash(setup_core: Path) -> None:
test_dir.mkdir()
result = cv.directory("test_dir/")
assert result == "test_dir/"
assert result == test_dir
result = cv.directory("test_dir")
assert result == "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"
assert cv.file_("config.yaml") == yaml_file
yml_file = setup_core / "config.yml"
yml_file.write_text("yml content")
assert cv.file_("config.yml") == "config.yml"
assert cv.file_("config.yml") == yml_file
txt_file = setup_core / "readme.txt"
txt_file.write_text("text content")
assert cv.file_("readme.txt") == "readme.txt"
assert cv.file_("readme.txt") == txt_file
no_ext_file = setup_core / "LICENSE"
no_ext_file.write_text("license content")
assert cv.file_("LICENSE") == "LICENSE"
assert cv.file_("LICENSE") == no_ext_file
def test_directory_with_symlink(setup_core: Path) -> None:
@@ -142,7 +142,7 @@ def test_directory_with_symlink(setup_core: Path) -> None:
symlink_dir.symlink_to(actual_dir)
result = cv.directory("symlink_directory")
assert result == "symlink_directory"
assert result == symlink_dir
def test_file_with_symlink(setup_core: Path) -> None:
@@ -154,7 +154,7 @@ def test_file_with_symlink(setup_core: Path) -> None:
symlink_file.symlink_to(actual_file)
result = cv.file_("symlink_file.txt")
assert result == "symlink_file.txt"
assert result == symlink_file
def test_directory_error_shows_full_path(setup_core: Path) -> None:
@@ -175,7 +175,7 @@ def test_directory_with_spaces_in_name(setup_core: Path) -> None:
dir_with_spaces.mkdir()
result = cv.directory("my test directory")
assert result == "my test directory"
assert result == dir_with_spaces
def test_file_with_spaces_in_name(setup_core: Path) -> None:
@@ -184,4 +184,4 @@ def test_file_with_spaces_in_name(setup_core: Path) -> None:
file_with_spaces.write_text("content")
result = cv.file_("my test file.yaml")
assert result == "my test file.yaml"
assert result == file_with_spaces

View File

@@ -1,4 +1,5 @@
import os
from pathlib import Path
from unittest.mock import patch
from hypothesis import given
@@ -536,8 +537,8 @@ class TestEsphomeCore:
@pytest.fixture
def target(self, fixture_path):
target = core.EsphomeCore()
target.build_path = "foo/build"
target.config_path = "foo/config"
target.build_path = Path("foo/build")
target.config_path = Path("foo/config")
return target
def test_reset(self, target):
@@ -584,33 +585,33 @@ class TestEsphomeCore:
@pytest.mark.skipif(os.name == "nt", reason="Unix-specific test")
def test_data_dir_default_unix(self, target):
"""Test data_dir returns .esphome in config directory by default on Unix."""
target.config_path = "/home/user/config.yaml"
assert target.data_dir == "/home/user/.esphome"
target.config_path = Path("/home/user/config.yaml")
assert target.data_dir == Path("/home/user/.esphome")
@pytest.mark.skipif(os.name != "nt", reason="Windows-specific test")
def test_data_dir_default_windows(self, target):
"""Test data_dir returns .esphome in config directory by default on Windows."""
target.config_path = "D:\\home\\user\\config.yaml"
assert target.data_dir == "D:\\home\\user\\.esphome"
target.config_path = Path("D:\\home\\user\\config.yaml")
assert target.data_dir == Path("D:\\home\\user\\.esphome")
def test_data_dir_ha_addon(self, target):
"""Test data_dir returns /data when running as Home Assistant addon."""
target.config_path = "/config/test.yaml"
target.config_path = Path("/config/test.yaml")
with patch.dict(os.environ, {"ESPHOME_IS_HA_ADDON": "true"}):
assert target.data_dir == "/data"
assert target.data_dir == Path("/data")
def test_data_dir_env_override(self, target):
"""Test data_dir uses ESPHOME_DATA_DIR environment variable when set."""
target.config_path = "/home/user/config.yaml"
target.config_path = Path("/home/user/config.yaml")
with patch.dict(os.environ, {"ESPHOME_DATA_DIR": "/custom/data/path"}):
assert target.data_dir == "/custom/data/path"
assert target.data_dir == Path("/custom/data/path")
@pytest.mark.skipif(os.name == "nt", reason="Unix-specific test")
def test_data_dir_priority_unix(self, target):
"""Test data_dir priority on Unix: HA addon > env var > default."""
target.config_path = "/config/test.yaml"
target.config_path = Path("/config/test.yaml")
expected_default = "/config/.esphome"
# Test HA addon takes priority over env var
@@ -618,26 +619,26 @@ class TestEsphomeCore:
os.environ,
{"ESPHOME_IS_HA_ADDON": "true", "ESPHOME_DATA_DIR": "/custom/path"},
):
assert target.data_dir == "/data"
assert target.data_dir == Path("/data")
# Test env var is used when not HA addon
with patch.dict(
os.environ,
{"ESPHOME_IS_HA_ADDON": "false", "ESPHOME_DATA_DIR": "/custom/path"},
):
assert target.data_dir == "/custom/path"
assert target.data_dir == Path("/custom/path")
# Test default when neither is set
with patch.dict(os.environ, {}, clear=True):
# Ensure these env vars are not set
os.environ.pop("ESPHOME_IS_HA_ADDON", None)
os.environ.pop("ESPHOME_DATA_DIR", None)
assert target.data_dir == expected_default
assert target.data_dir == Path(expected_default)
@pytest.mark.skipif(os.name != "nt", reason="Windows-specific test")
def test_data_dir_priority_windows(self, target):
"""Test data_dir priority on Windows: HA addon > env var > default."""
target.config_path = "D:\\config\\test.yaml"
target.config_path = Path("D:\\config\\test.yaml")
expected_default = "D:\\config\\.esphome"
# Test HA addon takes priority over env var
@@ -645,21 +646,21 @@ class TestEsphomeCore:
os.environ,
{"ESPHOME_IS_HA_ADDON": "true", "ESPHOME_DATA_DIR": "/custom/path"},
):
assert target.data_dir == "/data"
assert target.data_dir == Path("/data")
# Test env var is used when not HA addon
with patch.dict(
os.environ,
{"ESPHOME_IS_HA_ADDON": "false", "ESPHOME_DATA_DIR": "/custom/path"},
):
assert target.data_dir == "/custom/path"
assert target.data_dir == Path("/custom/path")
# Test default when neither is set
with patch.dict(os.environ, {}, clear=True):
# Ensure these env vars are not set
os.environ.pop("ESPHOME_IS_HA_ADDON", None)
os.environ.pop("ESPHOME_DATA_DIR", None)
assert target.data_dir == expected_default
assert target.data_dir == Path(expected_default)
def test_platformio_cache_dir_with_env_var(self):
"""Test platformio_cache_dir when PLATFORMIO_CACHE_DIR env var is set."""

View File

@@ -13,7 +13,12 @@ def test_coro_priority_enum_values() -> None:
assert CoroPriority.CORE == 100
assert CoroPriority.DIAGNOSTICS == 90
assert CoroPriority.STATUS == 80
assert CoroPriority.WEB_SERVER_BASE == 65
assert CoroPriority.CAPTIVE_PORTAL == 64
assert CoroPriority.COMMUNICATION == 60
assert CoroPriority.NETWORK_SERVICES == 55
assert CoroPriority.OTA_UPDATES == 54
assert CoroPriority.WEB_SERVER_OTA == 52
assert CoroPriority.APPLICATION == 50
assert CoroPriority.WEB == 40
assert CoroPriority.AUTOMATION == 30
@@ -70,7 +75,12 @@ def test_float_and_enum_are_interchangeable() -> None:
(CoroPriority.CORE, 100.0),
(CoroPriority.DIAGNOSTICS, 90.0),
(CoroPriority.STATUS, 80.0),
(CoroPriority.WEB_SERVER_BASE, 65.0),
(CoroPriority.CAPTIVE_PORTAL, 64.0),
(CoroPriority.COMMUNICATION, 60.0),
(CoroPriority.NETWORK_SERVICES, 55.0),
(CoroPriority.OTA_UPDATES, 54.0),
(CoroPriority.WEB_SERVER_OTA, 52.0),
(CoroPriority.APPLICATION, 50.0),
(CoroPriority.WEB, 40.0),
(CoroPriority.AUTOMATION, 30.0),
@@ -164,8 +174,13 @@ def test_enum_priority_comparison() -> None:
assert CoroPriority.NETWORK_TRANSPORT > CoroPriority.CORE
assert CoroPriority.CORE > CoroPriority.DIAGNOSTICS
assert CoroPriority.DIAGNOSTICS > CoroPriority.STATUS
assert CoroPriority.STATUS > CoroPriority.COMMUNICATION
assert CoroPriority.COMMUNICATION > CoroPriority.APPLICATION
assert CoroPriority.STATUS > CoroPriority.WEB_SERVER_BASE
assert CoroPriority.WEB_SERVER_BASE > CoroPriority.CAPTIVE_PORTAL
assert CoroPriority.CAPTIVE_PORTAL > CoroPriority.COMMUNICATION
assert CoroPriority.COMMUNICATION > CoroPriority.NETWORK_SERVICES
assert CoroPriority.NETWORK_SERVICES > CoroPriority.OTA_UPDATES
assert CoroPriority.OTA_UPDATES > CoroPriority.WEB_SERVER_OTA
assert CoroPriority.WEB_SERVER_OTA > CoroPriority.APPLICATION
assert CoroPriority.APPLICATION > CoroPriority.WEB
assert CoroPriority.WEB > CoroPriority.AUTOMATION
assert CoroPriority.AUTOMATION > CoroPriority.BUS

View File

@@ -42,7 +42,7 @@ def test_is_file_recent_with_recent_file(setup_core: Path) -> None:
refresh = TimePeriod(seconds=3600)
result = external_files.is_file_recent(str(test_file), refresh)
result = external_files.is_file_recent(test_file, refresh)
assert result is True
@@ -53,11 +53,13 @@ def test_is_file_recent_with_old_file(setup_core: Path) -> None:
test_file.write_text("content")
old_time = time.time() - 7200
mock_stat = MagicMock()
mock_stat.st_ctime = old_time
with patch("os.path.getctime", return_value=old_time):
with patch.object(Path, "stat", return_value=mock_stat):
refresh = TimePeriod(seconds=3600)
result = external_files.is_file_recent(str(test_file), refresh)
result = external_files.is_file_recent(test_file, refresh)
assert result is False
@@ -67,7 +69,7 @@ def test_is_file_recent_nonexistent_file(setup_core: Path) -> None:
test_file = setup_core / "nonexistent.txt"
refresh = TimePeriod(seconds=3600)
result = external_files.is_file_recent(str(test_file), refresh)
result = external_files.is_file_recent(test_file, refresh)
assert result is False
@@ -77,10 +79,12 @@ def test_is_file_recent_with_zero_refresh(setup_core: Path) -> None:
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):
# Mock stat to return a time 10 seconds ago
mock_stat = MagicMock()
mock_stat.st_ctime = time.time() - 10
with patch.object(Path, "stat", return_value=mock_stat):
refresh = TimePeriod(seconds=0)
result = external_files.is_file_recent(str(test_file), refresh)
result = external_files.is_file_recent(test_file, refresh)
assert result is False
@@ -97,7 +101,7 @@ def test_has_remote_file_changed_not_modified(
mock_head.return_value = mock_response
url = "https://example.com/file.txt"
result = external_files.has_remote_file_changed(url, str(test_file))
result = external_files.has_remote_file_changed(url, test_file)
assert result is False
mock_head.assert_called_once()
@@ -121,7 +125,7 @@ def test_has_remote_file_changed_modified(
mock_head.return_value = mock_response
url = "https://example.com/file.txt"
result = external_files.has_remote_file_changed(url, str(test_file))
result = external_files.has_remote_file_changed(url, test_file)
assert result is True
@@ -131,7 +135,7 @@ def test_has_remote_file_changed_no_local_file(setup_core: Path) -> None:
test_file = setup_core / "nonexistent.txt"
url = "https://example.com/file.txt"
result = external_files.has_remote_file_changed(url, str(test_file))
result = external_files.has_remote_file_changed(url, test_file)
assert result is True
@@ -149,7 +153,7 @@ def test_has_remote_file_changed_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))
external_files.has_remote_file_changed(url, test_file)
@patch("esphome.external_files.requests.head")
@@ -165,7 +169,7 @@ def test_has_remote_file_changed_timeout(
mock_head.return_value = mock_response
url = "https://example.com/file.txt"
external_files.has_remote_file_changed(url, str(test_file))
external_files.has_remote_file_changed(url, test_file)
call_args = mock_head.call_args
assert call_args[1]["timeout"] == external_files.NETWORK_TIMEOUT
@@ -191,6 +195,6 @@ def test_is_file_recent_handles_float_seconds(setup_core: Path) -> None:
refresh = TimePeriod(seconds=3600.5)
result = external_files.is_file_recent(str(test_file), refresh)
result = external_files.is_file_recent(test_file, refresh)
assert result is True

View File

@@ -154,11 +154,11 @@ def test_walk_files(fixture_path):
actual = list(helpers.walk_files(path))
# Ensure paths start with the root
assert all(p.startswith(str(path)) for p in actual)
assert all(p.is_relative_to(path) for p in actual)
class Test_write_file_if_changed:
def test_src_and_dst_match(self, tmp_path):
def test_src_and_dst_match(self, tmp_path: Path):
text = "A files are unique.\n"
initial = text
dst = tmp_path / "file-a.txt"
@@ -168,7 +168,7 @@ class Test_write_file_if_changed:
assert dst.read_text() == text
def test_src_and_dst_do_not_match(self, tmp_path):
def test_src_and_dst_do_not_match(self, tmp_path: Path):
text = "A files are unique.\n"
initial = "B files are unique.\n"
dst = tmp_path / "file-a.txt"
@@ -178,7 +178,7 @@ class Test_write_file_if_changed:
assert dst.read_text() == text
def test_dst_does_not_exist(self, tmp_path):
def test_dst_does_not_exist(self, tmp_path: Path):
text = "A files are unique.\n"
dst = tmp_path / "file-a.txt"
@@ -188,7 +188,7 @@ class Test_write_file_if_changed:
class Test_copy_file_if_changed:
def test_src_and_dst_match(self, tmp_path, fixture_path):
def test_src_and_dst_match(self, tmp_path: Path, fixture_path: Path):
src = fixture_path / "helpers" / "file-a.txt"
initial = fixture_path / "helpers" / "file-a.txt"
dst = tmp_path / "file-a.txt"
@@ -197,7 +197,7 @@ class Test_copy_file_if_changed:
helpers.copy_file_if_changed(src, dst)
def test_src_and_dst_do_not_match(self, tmp_path, fixture_path):
def test_src_and_dst_do_not_match(self, tmp_path: Path, fixture_path: Path):
src = fixture_path / "helpers" / "file-a.txt"
initial = fixture_path / "helpers" / "file-c.txt"
dst = tmp_path / "file-a.txt"
@@ -208,7 +208,7 @@ class Test_copy_file_if_changed:
assert src.read_text() == dst.read_text()
def test_dst_does_not_exist(self, tmp_path, fixture_path):
def test_dst_does_not_exist(self, tmp_path: Path, fixture_path: Path):
src = fixture_path / "helpers" / "file-a.txt"
dst = tmp_path / "file-a.txt"
@@ -604,9 +604,8 @@ def test_mkdir_p_with_existing_file_raises_error(tmp_path: Path) -> None:
helpers.mkdir_p(dir_path)
@pytest.mark.skipif(os.name == "nt", reason="Unix-specific test")
def test_read_file_unix(tmp_path: Path) -> None:
"""Test read_file reads file content correctly on Unix."""
def test_read_file(tmp_path: Path) -> None:
"""Test read_file reads file content correctly."""
# Test reading regular file
test_file = tmp_path / "test.txt"
expected_content = "Test content\nLine 2\n"
@@ -624,31 +623,10 @@ def test_read_file_unix(tmp_path: Path) -> None:
assert content == utf8_content
@pytest.mark.skipif(os.name != "nt", reason="Windows-specific test")
def test_read_file_windows(tmp_path: Path) -> None:
"""Test read_file reads file content correctly on Windows."""
# Test reading regular file
test_file = tmp_path / "test.txt"
expected_content = "Test content\nLine 2\n"
test_file.write_text(expected_content)
content = helpers.read_file(test_file)
# On Windows, text mode reading converts \n to \r\n
assert content == expected_content.replace("\n", "\r\n")
# Test reading file with UTF-8 characters
utf8_file = tmp_path / "utf8.txt"
utf8_content = "Hello 世界 🌍"
utf8_file.write_text(utf8_content, encoding="utf-8")
content = helpers.read_file(utf8_file)
assert content == utf8_content
def test_read_file_not_found() -> None:
"""Test read_file raises error for non-existent file."""
with pytest.raises(EsphomeError, match=r"Error reading file"):
helpers.read_file("/nonexistent/file.txt")
helpers.read_file(Path("/nonexistent/file.txt"))
def test_read_file_unicode_decode_error(tmp_path: Path) -> None:

View File

@@ -885,7 +885,7 @@ def test_upload_program_ota_success(
assert exit_code == 0
assert host == "192.168.1.100"
expected_firmware = str(
expected_firmware = (
tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin"
)
mock_run_ota.assert_called_once_with(
@@ -919,7 +919,9 @@ def test_upload_program_ota_with_file_arg(
assert exit_code == 0
assert host == "192.168.1.100"
mock_run_ota.assert_called_once_with(["192.168.1.100"], 3232, "", "custom.bin")
mock_run_ota.assert_called_once_with(
["192.168.1.100"], 3232, "", Path("custom.bin")
)
def test_upload_program_ota_no_config(
@@ -972,7 +974,7 @@ def test_upload_program_ota_with_mqtt_resolution(
assert exit_code == 0
assert host == "192.168.1.100"
mock_mqtt_get_ip.assert_called_once_with(config, "user", "pass", "client")
expected_firmware = str(
expected_firmware = (
tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin"
)
mock_run_ota.assert_called_once_with(["192.168.1.100"], 3232, "", expected_firmware)
@@ -1382,7 +1384,7 @@ def test_command_wizard(tmp_path: Path) -> None:
result = command_wizard(args)
assert result == 0
mock_wizard.assert_called_once_with(str(config_file))
mock_wizard.assert_called_once_with(config_file)
def test_command_rename_invalid_characters(
@@ -1407,7 +1409,7 @@ def test_command_rename_complex_yaml(
config_file = tmp_path / "test.yaml"
config_file.write_text("# Complex YAML without esphome section\nsome_key: value\n")
setup_core(tmp_path=tmp_path)
CORE.config_path = str(config_file)
CORE.config_path = config_file
args = MockArgs(name="newname")
result = command_rename(args, {})
@@ -1436,7 +1438,7 @@ wifi:
password: "test1234"
""")
setup_core(tmp_path=tmp_path)
CORE.config_path = str(config_file)
CORE.config_path = config_file
# Set up CORE.config to avoid ValueError when accessing CORE.address
CORE.config = {CONF_ESPHOME: {CONF_NAME: "oldname"}}
@@ -1486,7 +1488,7 @@ esp32:
board: nodemcu-32s
""")
setup_core(tmp_path=tmp_path)
CORE.config_path = str(config_file)
CORE.config_path = config_file
# Set up CORE.config to avoid ValueError when accessing CORE.address
CORE.config = {
@@ -1523,7 +1525,7 @@ esp32:
board: nodemcu-32s
""")
setup_core(tmp_path=tmp_path)
CORE.config_path = str(config_file)
CORE.config_path = config_file
args = MockArgs(name="newname", dashboard=False)

View File

@@ -15,45 +15,45 @@ from esphome.core import CORE, EsphomeError
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.build_path = 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"
assert idedata.firmware_elf_path == 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.build_path = 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 isinstance(result, Path)
expected = Path("/path/to/firmware.bin")
assert result == expected
assert result.endswith(".bin")
assert str(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.build_path = 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"))
expected = 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.build_path = setup_core / "build" / "test"
CORE.name = "test"
raw_data = {
"prog_path": "/path/to/firmware.elf",
@@ -69,15 +69,15 @@ def test_idedata_extra_flash_images(setup_core: Path) -> None:
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].path == Path("/path/to/bootloader.bin")
assert images[0].offset == "0x1000"
assert images[1].path == "/path/to/partition.bin"
assert images[1].path == 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.build_path = setup_core / "build" / "test"
CORE.name = "test"
raw_data = {"prog_path": "/path/to/firmware.elf", "extra": {"flash_images": []}}
idedata = platformio_api.IDEData(raw_data)
@@ -88,7 +88,7 @@ def test_idedata_extra_flash_images_empty(setup_core: Path) -> None:
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.build_path = setup_core / "build" / "test"
CORE.name = "test"
raw_data = {
"prog_path": "/path/to/firmware.elf",
@@ -104,9 +104,9 @@ def test_idedata_cc_path(setup_core: Path) -> None:
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")
image = platformio_api.FlashImage(path=Path("/path/to/image.bin"), offset="0x10000")
assert image.path == "/path/to/image.bin"
assert image.path == Path("/path/to/image.bin")
assert image.offset == "0x10000"
@@ -114,7 +114,7 @@ def test_load_idedata_returns_dict(
setup_core: Path, mock_run_platformio_cli_run
) -> None:
"""Test _load_idedata returns parsed idedata dict when successful."""
CORE.build_path = str(setup_core / "build" / "test")
CORE.build_path = setup_core / "build" / "test"
CORE.name = "test"
# Create required files
@@ -366,7 +366,7 @@ def test_get_idedata_caches_result(
assert result1 is result2
assert isinstance(result1, platformio_api.IDEData)
assert result1.firmware_elf_path == "/test/firmware.elf"
assert result1.firmware_elf_path == Path("/test/firmware.elf")
def test_idedata_addr2line_path_windows(setup_core: Path) -> None:
@@ -434,9 +434,9 @@ def test_patched_clean_build_dir_removes_outdated(setup_core: Path) -> None:
os.utime(platformio_ini, (build_mtime + 1, build_mtime + 1))
# Track if directory was removed
removed_paths: list[str] = []
removed_paths: list[Path] = []
def track_rmtree(path: str) -> None:
def track_rmtree(path: Path) -> None:
removed_paths.append(path)
shutil.rmtree(path)
@@ -466,7 +466,7 @@ def test_patched_clean_build_dir_removes_outdated(setup_core: Path) -> None:
# Verify directory was removed and recreated
assert len(removed_paths) == 1
assert removed_paths[0] == str(build_dir)
assert removed_paths[0] == build_dir
assert build_dir.exists() # makedirs recreated it

View File

@@ -15,12 +15,12 @@ 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")
CORE.config_path = 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")
expected = data_dir / "storage" / "my_device.yaml.json"
assert result == expected
@@ -29,20 +29,20 @@ def test_ext_storage_path(setup_core: Path) -> None:
result = storage_json.ext_storage_path("other_device.yaml")
data_dir = Path(CORE.data_dir)
expected = str(data_dir / "storage" / "other_device.yaml.json")
expected = 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")
assert str(result_yml).endswith("device.yml.json")
result_no_ext = storage_json.ext_storage_path("device")
assert result_no_ext.endswith("device.json")
assert str(result_no_ext).endswith("device.json")
result_path = storage_json.ext_storage_path("my/device.yaml")
assert result_path.endswith("device.yaml.json")
assert str(result_path).endswith("device.yaml.json")
def test_esphome_storage_path(setup_core: Path) -> None:
@@ -50,7 +50,7 @@ def test_esphome_storage_path(setup_core: Path) -> None:
result = storage_json.esphome_storage_path()
data_dir = Path(CORE.data_dir)
expected = str(data_dir / "esphome.json")
expected = data_dir / "esphome.json"
assert result == expected
@@ -59,27 +59,27 @@ def test_ignored_devices_storage_path(setup_core: Path) -> None:
result = storage_json.ignored_devices_storage_path()
data_dir = Path(CORE.data_dir)
expected = str(data_dir / "ignored-devices.json")
expected = 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")
CORE.config_path = setup_core / "configs" / "device.yaml"
result = storage_json.trash_storage_path()
expected = str(setup_core / "configs" / "trash")
expected = 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")
CORE.config_path = setup_core / "configs" / "device.yaml"
result = storage_json.archive_storage_path()
expected = str(setup_core / "configs" / "archive")
expected = setup_core / "configs" / "archive"
assert result == expected
@@ -87,12 +87,12 @@ 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")
CORE.config_path = subdir / "sensor.yaml"
result = storage_json.storage_path()
data_dir = Path(CORE.data_dir)
expected = str(data_dir / "storage" / "sensor.yaml.json")
expected = data_dir / "storage" / "sensor.yaml.json"
assert result == expected
@@ -173,16 +173,16 @@ def test_storage_paths_with_ha_addon(mock_is_ha_addon: bool, tmp_path: Path) ->
"""Test storage paths when running as Home Assistant addon."""
mock_is_ha_addon.return_value = True
CORE.config_path = str(tmp_path / "test.yaml")
CORE.config_path = 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")
expected = Path("/data") / "storage" / "test.yaml.json"
assert result == expected
result = storage_json.esphome_storage_path()
expected = str(Path("/data") / "esphome.json")
expected = Path("/data") / "esphome.json"
assert result == expected
@@ -375,7 +375,7 @@ def test_storage_json_load_valid_file(tmp_path: Path) -> None:
file_path = tmp_path / "storage.json"
file_path.write_text(json.dumps(storage_data))
result = storage_json.StorageJSON.load(str(file_path))
result = storage_json.StorageJSON.load(file_path)
assert result is not None
assert result.name == "loaded_device"
@@ -386,8 +386,8 @@ def test_storage_json_load_valid_file(tmp_path: Path) -> None:
assert result.address == "10.0.0.1"
assert result.web_port == 8080
assert result.target_platform == "ESP32"
assert result.build_path == "/loaded/build"
assert result.firmware_bin_path == "/loaded/firmware.bin"
assert result.build_path == Path("/loaded/build")
assert result.firmware_bin_path == Path("/loaded/firmware.bin")
assert result.loaded_integrations == {"wifi", "api"}
assert result.loaded_platforms == {"sensor"}
assert result.no_mdns is True
@@ -400,7 +400,7 @@ def test_storage_json_load_invalid_file(tmp_path: Path) -> None:
file_path = tmp_path / "invalid.json"
file_path.write_text("not valid json{")
result = storage_json.StorageJSON.load(str(file_path))
result = storage_json.StorageJSON.load(file_path)
assert result is None
@@ -654,7 +654,7 @@ def test_storage_json_load_legacy_esphomeyaml_version(tmp_path: Path) -> None:
file_path = tmp_path / "legacy.json"
file_path.write_text(json.dumps(storage_data))
result = storage_json.StorageJSON.load(str(file_path))
result = storage_json.StorageJSON.load(file_path)
assert result is not None
assert result.esphome_version == "1.14.0" # Should map to esphome_version

View File

@@ -1,6 +1,6 @@
import glob
import logging
import os
from pathlib import Path
from esphome import yaml_util
from esphome.components import substitutions
@@ -52,9 +52,8 @@ def dict_diff(a, b, path=""):
return diffs
def write_yaml(path, data):
with open(path, "w", encoding="utf-8") as f:
f.write(yaml_util.dump(data))
def write_yaml(path: Path, data: dict) -> None:
path.write_text(yaml_util.dump(data), encoding="utf-8")
def test_substitutions_fixtures(fixture_path):
@@ -64,11 +63,10 @@ def test_substitutions_fixtures(fixture_path):
failures = []
for source_path in sources:
source_path = Path(source_path)
try:
expected_path = source_path.replace(".input.yaml", ".approved.yaml")
test_case = os.path.splitext(os.path.basename(source_path))[0].replace(
".input", ""
)
expected_path = source_path.with_suffix("").with_suffix(".approved.yaml")
test_case = source_path.with_suffix("").stem
# Load using ESPHome's YAML loader
config = yaml_util.load_yaml(source_path)
@@ -81,12 +79,12 @@ def test_substitutions_fixtures(fixture_path):
substitutions.do_substitution_pass(config, None)
# Also load expected using ESPHome's loader, or use {} if missing and DEV_MODE
if os.path.isfile(expected_path):
if expected_path.is_file():
expected = yaml_util.load_yaml(expected_path)
elif DEV_MODE:
expected = {}
else:
assert os.path.isfile(expected_path), (
assert expected_path.is_file(), (
f"Expected file missing: {expected_path}"
)
@@ -97,16 +95,14 @@ def test_substitutions_fixtures(fixture_path):
if got_sorted != expected_sorted:
diff = "\n".join(dict_diff(got_sorted, expected_sorted))
msg = (
f"Substitution result mismatch for {os.path.basename(source_path)}\n"
f"Substitution result mismatch for {source_path.name}\n"
f"Diff:\n{diff}\n\n"
f"Got: {got_sorted}\n"
f"Expected: {expected_sorted}"
)
# Write out the received file when test fails
if DEV_MODE:
received_path = os.path.join(
os.path.dirname(source_path), f"{test_case}.received.yaml"
)
received_path = source_path.with_name(f"{test_case}.received.yaml")
write_yaml(received_path, config)
print(msg)
failures.append(msg)

View File

@@ -32,21 +32,21 @@ def test_list_yaml_files_with_files_and_directories(tmp_path: Path) -> None:
# Test with mixed input (directories and files)
configs = [
str(dir1),
str(standalone1),
str(dir2),
str(standalone2),
dir1,
standalone1,
dir2,
standalone2,
]
result = util.list_yaml_files(configs)
# Should include all YAML files but not the .txt file
assert set(result) == {
str(dir1 / "config1.yaml"),
str(dir1 / "config2.yml"),
str(dir2 / "config3.yaml"),
str(standalone1),
str(standalone2),
dir1 / "config1.yaml",
dir1 / "config2.yml",
dir2 / "config3.yaml",
standalone1,
standalone2,
}
# Check that results are sorted
assert result == sorted(result)
@@ -63,12 +63,12 @@ def test_list_yaml_files_only_directories(tmp_path: Path) -> None:
(dir1 / "b.yml").write_text("test: b")
(dir2 / "c.yaml").write_text("test: c")
result = util.list_yaml_files([str(dir1), str(dir2)])
result = util.list_yaml_files([dir1, dir2])
assert set(result) == {
str(dir1 / "a.yaml"),
str(dir1 / "b.yml"),
str(dir2 / "c.yaml"),
dir1 / "a.yaml",
dir1 / "b.yml",
dir2 / "c.yaml",
}
assert result == sorted(result)
@@ -88,17 +88,17 @@ def test_list_yaml_files_only_files(tmp_path: Path) -> None:
# Include a non-YAML file to test filtering
result = util.list_yaml_files(
[
str(file1),
str(file2),
str(file3),
str(non_yaml),
file1,
file2,
file3,
non_yaml,
]
)
assert set(result) == {
str(file1),
str(file2),
str(file3),
file1,
file2,
file3,
}
assert result == sorted(result)
@@ -108,7 +108,7 @@ def test_list_yaml_files_empty_directory(tmp_path: Path) -> None:
empty_dir = tmp_path / "empty"
empty_dir.mkdir()
result = util.list_yaml_files([str(empty_dir)])
result = util.list_yaml_files([empty_dir])
assert result == []
@@ -121,7 +121,7 @@ def test_list_yaml_files_nonexistent_path(tmp_path: Path) -> None:
# Should raise an error for non-existent directory
with pytest.raises(FileNotFoundError):
util.list_yaml_files([str(nonexistent), str(existing)])
util.list_yaml_files([nonexistent, existing])
def test_list_yaml_files_mixed_extensions(tmp_path: Path) -> None:
@@ -137,11 +137,11 @@ def test_list_yaml_files_mixed_extensions(tmp_path: Path) -> None:
yml_file.write_text("test: yml")
other_file.write_text("test: txt")
result = util.list_yaml_files([str(dir1)])
result = util.list_yaml_files([dir1])
assert set(result) == {
str(yaml_file),
str(yml_file),
yaml_file,
yml_file,
}
@@ -174,17 +174,18 @@ def test_list_yaml_files_does_not_recurse_into_subdirectories(tmp_path: Path) ->
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
assert root / "config1.yaml" in result
assert root / "config2.yml" in result
assert 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
r_str = str(r)
assert "subdir" not in r_str
assert "deeper" not in r_str
assert "nested1.yaml" not in r_str
assert "nested2.yml" not in r_str
assert "very_nested.yaml" not in r_str
def test_list_yaml_files_excludes_secrets(tmp_path: Path) -> None:
@@ -202,10 +203,10 @@ def test_list_yaml_files_excludes_secrets(tmp_path: Path) -> None:
# 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
assert root / "config.yaml" in result
assert root / "device.yaml" in result
assert root / "secrets.yaml" not in result
assert root / "secrets.yml" not in result
def test_list_yaml_files_excludes_hidden_files(tmp_path: Path) -> None:
@@ -223,93 +224,102 @@ def test_list_yaml_files_excludes_hidden_files(tmp_path: Path) -> None:
# 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
assert root / "config.yaml" in result
assert root / "device.yaml" in result
assert root / ".hidden.yaml" not in result
assert 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",
Path("/path/to/config.yaml"),
Path("/path/to/device.yml"),
Path("/path/to/readme.txt"),
Path("/path/to/script.py"),
Path("/path/to/data.json"),
Path("/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
assert Path("/path/to/config.yaml") in result
assert Path("/path/to/device.yml") in result
assert Path("/path/to/another.yaml") in result
assert Path("/path/to/readme.txt") not in result
assert Path("/path/to/script.py") not in result
assert Path("/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",
Path("/path/to/config.yaml"),
Path("/path/to/secrets.yaml"),
Path("/path/to/secrets.yml"),
Path("/path/to/device.yaml"),
Path("/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
assert Path("/path/to/config.yaml") in result
assert Path("/path/to/device.yaml") in result
assert Path("/path/to/secrets.yaml") not in result
assert Path("/path/to/secrets.yml") not in result
assert Path("/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",
Path("/path/to/config.yaml"),
Path("/path/to/.hidden.yaml"),
Path("/path/to/.backup.yml"),
Path("/path/to/device.yaml"),
Path("/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
assert Path("/path/to/config.yaml") in result
assert Path("/path/to/device.yaml") in result
assert Path("/path/to/.hidden.yaml") not in result
assert Path("/path/to/.backup.yml") not in result
assert Path("/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",
Path("/path/to/config.yaml"),
Path("/path/to/config.YAML"),
Path("/path/to/config.YML"),
Path("/path/to/config.Yaml"),
Path("/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
# Check the actual suffixes to ensure case-sensitive filtering
result_suffixes = [p.suffix for p in result]
assert ".yaml" in result_suffixes
assert ".yml" in result_suffixes
# Verify the filtered files have the expected names
result_names = [p.name for p in result]
assert "config.yaml" in result_names
assert "config.yml" in result_names
# Ensure uppercase extensions are NOT included
assert "config.YAML" not in result_names
assert "config.YML" not in result_names
assert "config.Yaml" not in result_names
@pytest.mark.parametrize(

View File

@@ -1,5 +1,5 @@
import json
import os
from pathlib import Path
from unittest.mock import Mock, patch
from esphome import vscode
@@ -45,7 +45,7 @@ RESULT_NO_ERROR = '{"type": "result", "yaml_errors": [], "validation_errors": []
def test_multi_file():
source_path = os.path.join("dir_path", "x.yaml")
source_path = str(Path("dir_path", "x.yaml"))
output_lines = _run_repl_test(
[
_validate(source_path),
@@ -62,7 +62,7 @@ esp8266:
expected_lines = [
_read_file(source_path),
_read_file(os.path.join("dir_path", "secrets.yaml")),
_read_file(str(Path("dir_path", "secrets.yaml"))),
RESULT_NO_ERROR,
]
@@ -70,7 +70,7 @@ esp8266:
def test_shows_correct_range_error():
source_path = os.path.join("dir_path", "x.yaml")
source_path = str(Path("dir_path", "x.yaml"))
output_lines = _run_repl_test(
[
_validate(source_path),
@@ -98,7 +98,7 @@ esp8266:
def test_shows_correct_loaded_file_error():
source_path = os.path.join("dir_path", "x.yaml")
source_path = str(Path("dir_path", "x.yaml"))
output_lines = _run_repl_test(
[
_validate(source_path),
@@ -121,7 +121,7 @@ packages:
validation_error = error["validation_errors"][0]
assert validation_error["message"].startswith("[broad] is an invalid option for")
range = validation_error["range"]
assert range["document"] == os.path.join("dir_path", ".pkg.esp8266.yaml")
assert range["document"] == str(Path("dir_path", ".pkg.esp8266.yaml"))
assert range["start_line"] == 1
assert range["start_col"] == 2
assert range["end_line"] == 1

View File

@@ -1,6 +1,5 @@
"""Tests for the wizard.py file."""
import os
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock
@@ -127,7 +126,7 @@ def test_wizard_write_sets_platform(
# Given
del default_config["platform"]
monkeypatch.setattr(wz, "write_file", MagicMock())
monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
monkeypatch.setattr(CORE, "config_path", tmp_path.parent)
# When
wz.wizard_write(tmp_path, **default_config)
@@ -147,7 +146,7 @@ def test_wizard_empty_config(tmp_path: Path, monkeypatch: MonkeyPatch):
"name": "test-empty",
}
monkeypatch.setattr(wz, "write_file", MagicMock())
monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
monkeypatch.setattr(CORE, "config_path", tmp_path.parent)
# When
wz.wizard_write(tmp_path, **empty_config)
@@ -168,7 +167,7 @@ def test_wizard_upload_config(tmp_path: Path, monkeypatch: MonkeyPatch):
"file_text": "# imported file 📁\n\n",
}
monkeypatch.setattr(wz, "write_file", MagicMock())
monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
monkeypatch.setattr(CORE, "config_path", tmp_path.parent)
# When
wz.wizard_write(tmp_path, **empty_config)
@@ -189,7 +188,7 @@ def test_wizard_write_defaults_platform_from_board_esp8266(
default_config["board"] = [*ESP8266_BOARD_PINS][0]
monkeypatch.setattr(wz, "write_file", MagicMock())
monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
monkeypatch.setattr(CORE, "config_path", tmp_path.parent)
# When
wz.wizard_write(tmp_path, **default_config)
@@ -210,7 +209,7 @@ def test_wizard_write_defaults_platform_from_board_esp32(
default_config["board"] = [*ESP32_BOARD_PINS][0]
monkeypatch.setattr(wz, "write_file", MagicMock())
monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
monkeypatch.setattr(CORE, "config_path", tmp_path.parent)
# When
wz.wizard_write(tmp_path, **default_config)
@@ -231,7 +230,7 @@ def test_wizard_write_defaults_platform_from_board_bk72xx(
default_config["board"] = [*BK72XX_BOARD_PINS][0]
monkeypatch.setattr(wz, "write_file", MagicMock())
monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
monkeypatch.setattr(CORE, "config_path", tmp_path.parent)
# When
wz.wizard_write(tmp_path, **default_config)
@@ -252,7 +251,7 @@ def test_wizard_write_defaults_platform_from_board_ln882x(
default_config["board"] = [*LN882X_BOARD_PINS][0]
monkeypatch.setattr(wz, "write_file", MagicMock())
monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
monkeypatch.setattr(CORE, "config_path", tmp_path.parent)
# When
wz.wizard_write(tmp_path, **default_config)
@@ -273,7 +272,7 @@ def test_wizard_write_defaults_platform_from_board_rtl87xx(
default_config["board"] = [*RTL87XX_BOARD_PINS][0]
monkeypatch.setattr(wz, "write_file", MagicMock())
monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
monkeypatch.setattr(CORE, "config_path", tmp_path.parent)
# When
wz.wizard_write(tmp_path, **default_config)
@@ -362,7 +361,7 @@ def test_wizard_rejects_path_with_invalid_extension():
"""
# Given
config_file = "test.json"
config_file = Path("test.json")
# When
retval = wz.wizard(config_file)
@@ -371,31 +370,31 @@ def test_wizard_rejects_path_with_invalid_extension():
assert retval == 1
def test_wizard_rejects_existing_files(tmpdir):
def test_wizard_rejects_existing_files(tmp_path):
"""
The wizard should reject any configuration file that already exists
"""
# Given
config_file = tmpdir.join("test.yaml")
config_file.write("")
config_file = tmp_path / "test.yaml"
config_file.write_text("")
# When
retval = wz.wizard(str(config_file))
retval = wz.wizard(config_file)
# Then
assert retval == 2
def test_wizard_accepts_default_answers_esp8266(
tmpdir, monkeypatch: MonkeyPatch, wizard_answers: list[str]
tmp_path: Path, monkeypatch: MonkeyPatch, wizard_answers: list[str]
):
"""
The wizard should accept the given default answers for esp8266
"""
# Given
config_file = tmpdir.join("test.yaml")
config_file = tmp_path / "test.yaml"
input_mock = MagicMock(side_effect=wizard_answers)
monkeypatch.setattr("builtins.input", input_mock)
monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0)
@@ -403,14 +402,14 @@ def test_wizard_accepts_default_answers_esp8266(
monkeypatch.setattr(wz, "wizard_write", MagicMock())
# When
retval = wz.wizard(str(config_file))
retval = wz.wizard(config_file)
# Then
assert retval == 0
def test_wizard_accepts_default_answers_esp32(
tmpdir, monkeypatch: MonkeyPatch, wizard_answers: list[str]
tmp_path: Path, monkeypatch: MonkeyPatch, wizard_answers: list[str]
):
"""
The wizard should accept the given default answers for esp32
@@ -419,7 +418,7 @@ def test_wizard_accepts_default_answers_esp32(
# Given
wizard_answers[1] = "ESP32"
wizard_answers[2] = "nodemcu-32s"
config_file = tmpdir.join("test.yaml")
config_file = tmp_path / "test.yaml"
input_mock = MagicMock(side_effect=wizard_answers)
monkeypatch.setattr("builtins.input", input_mock)
monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0)
@@ -427,14 +426,14 @@ def test_wizard_accepts_default_answers_esp32(
monkeypatch.setattr(wz, "wizard_write", MagicMock())
# When
retval = wz.wizard(str(config_file))
retval = wz.wizard(config_file)
# Then
assert retval == 0
def test_wizard_offers_better_node_name(
tmpdir, monkeypatch: MonkeyPatch, wizard_answers: list[str]
tmp_path: Path, monkeypatch: MonkeyPatch, wizard_answers: list[str]
):
"""
When the node name does not conform, a better alternative is offered
@@ -451,7 +450,7 @@ def test_wizard_offers_better_node_name(
wz, "default_input", MagicMock(side_effect=lambda _, default: default)
)
config_file = tmpdir.join("test.yaml")
config_file = tmp_path / "test.yaml"
input_mock = MagicMock(side_effect=wizard_answers)
monkeypatch.setattr("builtins.input", input_mock)
monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0)
@@ -459,7 +458,7 @@ def test_wizard_offers_better_node_name(
monkeypatch.setattr(wz, "wizard_write", MagicMock())
# When
retval = wz.wizard(str(config_file))
retval = wz.wizard(config_file)
# Then
assert retval == 0
@@ -467,7 +466,7 @@ def test_wizard_offers_better_node_name(
def test_wizard_requires_correct_platform(
tmpdir, monkeypatch: MonkeyPatch, wizard_answers: list[str]
tmp_path: Path, monkeypatch: MonkeyPatch, wizard_answers: list[str]
):
"""
When the platform is not either esp32 or esp8266, the wizard should reject it
@@ -476,7 +475,7 @@ def test_wizard_requires_correct_platform(
# Given
wizard_answers.insert(1, "foobar") # add invalid entry for platform
config_file = tmpdir.join("test.yaml")
config_file = tmp_path / "test.yaml"
input_mock = MagicMock(side_effect=wizard_answers)
monkeypatch.setattr("builtins.input", input_mock)
monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0)
@@ -484,14 +483,14 @@ def test_wizard_requires_correct_platform(
monkeypatch.setattr(wz, "wizard_write", MagicMock())
# When
retval = wz.wizard(str(config_file))
retval = wz.wizard(config_file)
# Then
assert retval == 0
def test_wizard_requires_correct_board(
tmpdir, monkeypatch: MonkeyPatch, wizard_answers: list[str]
tmp_path: Path, monkeypatch: MonkeyPatch, wizard_answers: list[str]
):
"""
When the board is not a valid esp8266 board, the wizard should reject it
@@ -500,7 +499,7 @@ def test_wizard_requires_correct_board(
# Given
wizard_answers.insert(2, "foobar") # add an invalid entry for board
config_file = tmpdir.join("test.yaml")
config_file = tmp_path / "test.yaml"
input_mock = MagicMock(side_effect=wizard_answers)
monkeypatch.setattr("builtins.input", input_mock)
monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0)
@@ -508,14 +507,14 @@ def test_wizard_requires_correct_board(
monkeypatch.setattr(wz, "wizard_write", MagicMock())
# When
retval = wz.wizard(str(config_file))
retval = wz.wizard(config_file)
# Then
assert retval == 0
def test_wizard_requires_valid_ssid(
tmpdir, monkeypatch: MonkeyPatch, wizard_answers: list[str]
tmp_path: Path, monkeypatch: MonkeyPatch, wizard_answers: list[str]
):
"""
When the board is not a valid esp8266 board, the wizard should reject it
@@ -524,7 +523,7 @@ def test_wizard_requires_valid_ssid(
# Given
wizard_answers.insert(3, "") # add an invalid entry for ssid
config_file = tmpdir.join("test.yaml")
config_file = tmp_path / "test.yaml"
input_mock = MagicMock(side_effect=wizard_answers)
monkeypatch.setattr("builtins.input", input_mock)
monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0)
@@ -532,28 +531,28 @@ def test_wizard_requires_valid_ssid(
monkeypatch.setattr(wz, "wizard_write", MagicMock())
# When
retval = wz.wizard(str(config_file))
retval = wz.wizard(config_file)
# Then
assert retval == 0
def test_wizard_write_protects_existing_config(
tmpdir, default_config: dict[str, Any], monkeypatch: MonkeyPatch
tmp_path: Path, default_config: dict[str, Any], monkeypatch: MonkeyPatch
):
"""
The wizard_write function should not overwrite existing config files and return False
"""
# Given
config_file = tmpdir.join("test.yaml")
config_file = tmp_path / "test.yaml"
original_content = "# Original config content\n"
config_file.write(original_content)
config_file.write_text(original_content)
monkeypatch.setattr(CORE, "config_path", str(tmpdir))
monkeypatch.setattr(CORE, "config_path", tmp_path.parent)
# When
result = wz.wizard_write(str(config_file), **default_config)
result = wz.wizard_write(config_file, **default_config)
# Then
assert result is False # Should return False when file exists
assert config_file.read() == original_content
assert config_file.read_text() == original_content

View File

@@ -257,10 +257,7 @@ def test_clean_cmake_cache(
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.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.name = "test_device"
# Verify file exists before
@@ -288,7 +285,7 @@ def test_clean_cmake_cache_no_pioenvs_dir(
pioenvs_dir = tmp_path / ".pioenvs"
# Setup mocks
mock_core.relative_pioenvs_path.return_value = str(pioenvs_dir)
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
# Verify directory doesn't exist
assert not pioenvs_dir.exists()
@@ -314,10 +311,7 @@ def test_clean_cmake_cache_no_cmake_file(
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.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.name = "test_device"
# Verify file doesn't exist
@@ -358,9 +352,9 @@ def test_clean_build(
(platformio_cache_dir / "downloads" / "package.tar.gz").write_text("package")
# 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)
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir
mock_core.relative_build_path.return_value = dependencies_lock
mock_core.platformio_cache_dir = str(platformio_cache_dir)
# Verify all exist before
@@ -409,9 +403,9 @@ def test_clean_build_partial_exists(
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)
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir
mock_core.relative_build_path.return_value = dependencies_lock
# Verify only pioenvs exists
assert pioenvs_dir.exists()
@@ -446,9 +440,9 @@ def test_clean_build_nothing_exists(
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)
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir
mock_core.relative_build_path.return_value = dependencies_lock
# Verify nothing exists
assert not pioenvs_dir.exists()
@@ -482,9 +476,9 @@ def test_clean_build_platformio_not_available(
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)
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir
mock_core.relative_build_path.return_value = dependencies_lock
# Verify all exist before
assert pioenvs_dir.exists()
@@ -520,9 +514,9 @@ def test_clean_build_empty_cache_dir(
pioenvs_dir.mkdir()
# Setup mocks
mock_core.relative_pioenvs_path.return_value = str(pioenvs_dir)
mock_core.relative_piolibdeps_path.return_value = str(tmp_path / ".piolibdeps")
mock_core.relative_build_path.return_value = str(tmp_path / "dependencies.lock")
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.relative_piolibdeps_path.return_value = tmp_path / ".piolibdeps"
mock_core.relative_build_path.return_value = tmp_path / "dependencies.lock"
# Verify pioenvs exists before
assert pioenvs_dir.exists()
@@ -553,7 +547,7 @@ def test_write_gitignore_creates_new_file(
gitignore_path = tmp_path / ".gitignore"
# Setup mocks
mock_core.relative_config_path.return_value = str(gitignore_path)
mock_core.relative_config_path.return_value = gitignore_path
# Verify file doesn't exist
assert not gitignore_path.exists()
@@ -577,7 +571,7 @@ def test_write_gitignore_skips_existing_file(
gitignore_path.write_text(existing_content)
# Setup mocks
mock_core.relative_config_path.return_value = str(gitignore_path)
mock_core.relative_config_path.return_value = gitignore_path
# Verify file exists with custom content
assert gitignore_path.exists()
@@ -616,7 +610,7 @@ void loop() {{}}"""
main_cpp.write_text(existing_content)
# Setup mocks
mock_core.relative_src_path.return_value = str(main_cpp)
mock_core.relative_src_path.return_value = main_cpp
mock_core.cpp_global_section = "// Global section"
# Call the function
@@ -653,7 +647,7 @@ def test_write_cpp_creates_new_file(
main_cpp = tmp_path / "main.cpp"
# Setup mocks
mock_core.relative_src_path.return_value = str(main_cpp)
mock_core.relative_src_path.return_value = main_cpp
mock_core.cpp_global_section = "// Global section"
# Verify file doesn't exist
@@ -669,7 +663,7 @@ def test_write_cpp_creates_new_file(
# 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)
assert written_path == main_cpp
# Check that all necessary parts are in the new file
assert '#include "esphome.h"' in written_content
@@ -699,7 +693,7 @@ def test_write_cpp_with_missing_end_marker(
main_cpp.write_text(existing_content)
# Setup mocks
mock_core.relative_src_path.return_value = str(main_cpp)
mock_core.relative_src_path.return_value = main_cpp
# Call should raise an error
with pytest.raises(EsphomeError, match="Could not find auto generated code end"):
@@ -725,7 +719,7 @@ def test_write_cpp_with_duplicate_markers(
main_cpp.write_text(existing_content)
# Setup mocks
mock_core.relative_src_path.return_value = str(main_cpp)
mock_core.relative_src_path.return_value = main_cpp
# Call should raise an error
with pytest.raises(EsphomeError, match="Found multiple auto generate code begins"):

View File

@@ -67,18 +67,18 @@ def test_parsing_with_custom_loader(fixture_path):
"""
yaml_file = fixture_path / "yaml_util" / "includetest.yaml"
loader_calls = []
loader_calls: list[Path] = []
def custom_loader(fname):
def custom_loader(fname: Path):
loader_calls.append(fname)
with open(yaml_file, encoding="utf-8") as f_handle:
with yaml_file.open(encoding="utf-8") as f_handle:
yaml_util.parse_yaml(yaml_file, f_handle, custom_loader)
assert len(loader_calls) == 3
assert loader_calls[0].endswith("includes/included.yaml")
assert loader_calls[1].endswith("includes/list.yaml")
assert loader_calls[2].endswith("includes/scalar.yaml")
assert loader_calls[0].parts[-2:] == ("includes", "included.yaml")
assert loader_calls[1].parts[-2:] == ("includes", "list.yaml")
assert loader_calls[2].parts[-2:] == ("includes", "scalar.yaml")
def test_construct_secret_simple(fixture_path: Path) -> None:
@@ -110,7 +110,7 @@ wifi:
secrets_yaml.write_text("some_other_secret: value")
with pytest.raises(EsphomeError, match="Secret 'nonexistent_secret' not defined"):
yaml_util.load_yaml(str(test_yaml))
yaml_util.load_yaml(test_yaml)
def test_construct_secret_no_secrets_file(tmp_path: Path) -> None:
@@ -124,10 +124,10 @@ wifi:
# Mock CORE.config_path to avoid NoneType error
with (
patch.object(core.CORE, "config_path", str(tmp_path / "main.yaml")),
patch.object(core.CORE, "config_path", tmp_path / "main.yaml"),
pytest.raises(EsphomeError, match="secrets.yaml"),
):
yaml_util.load_yaml(str(test_yaml))
yaml_util.load_yaml(test_yaml)
def test_construct_secret_fallback_to_main_config_dir(
@@ -149,8 +149,8 @@ wifi:
main_secrets.write_text("test_secret: main_secret_value")
# Mock CORE.config_path to point to main directory
with patch.object(core.CORE, "config_path", str(tmp_path / "main.yaml")):
actual = yaml_util.load_yaml(str(test_yaml))
with patch.object(core.CORE, "config_path", tmp_path / "main.yaml"):
actual = yaml_util.load_yaml(test_yaml)
assert actual["wifi"]["password"] == "main_secret_value"
@@ -167,7 +167,7 @@ def test_construct_include_dir_named(fixture_path: Path, tmp_path: Path) -> None
sensor: !include_dir_named named_dir
""")
actual = yaml_util.load_yaml(str(test_yaml))
actual = yaml_util.load_yaml(test_yaml)
actual_sensor = actual["sensor"]
# Check that files were loaded with their names as keys
@@ -202,7 +202,7 @@ def test_construct_include_dir_named_empty_dir(tmp_path: Path) -> None:
sensor: !include_dir_named empty_dir
""")
actual = yaml_util.load_yaml(str(test_yaml))
actual = yaml_util.load_yaml(test_yaml)
# Should return empty OrderedDict
assert isinstance(actual["sensor"], OrderedDict)
@@ -234,7 +234,7 @@ def test_construct_include_dir_named_with_dots(tmp_path: Path) -> None:
test: !include_dir_named test_dir
""")
actual = yaml_util.load_yaml(str(test_yaml))
actual = yaml_util.load_yaml(test_yaml)
# Should only include visible file
assert "visible" in actual["test"]
@@ -258,7 +258,7 @@ def test_find_files_recursive(fixture_path: Path, tmp_path: Path) -> None:
all_sensors: !include_dir_named named_dir
""")
actual = yaml_util.load_yaml(str(test_yaml))
actual = yaml_util.load_yaml(test_yaml)
# Should find sensor1.yaml, sensor2.yaml, and subdir/sensor3.yaml (all flattened)
assert len(actual["all_sensors"]) == 3