Merge branch 'esp8266-native-pch' into esp32-idf-pch

This commit is contained in:
J. Nick Koston
2026-08-26 22:06:04 -05:00
144 changed files with 7653 additions and 644 deletions
-28
View File
@@ -3,7 +3,6 @@
from collections.abc import Callable
import os
from pathlib import Path
import types
from typing import Any
from unittest.mock import MagicMock, Mock, patch
@@ -705,33 +704,6 @@ def test_include_file_with_c_header(
assert '#include "c_library.h"' in mock_raw_statement.text
def test_get_usable_cpu_count() -> None:
"""Test get_usable_cpu_count returns CPU count."""
count = config.get_usable_cpu_count()
assert isinstance(count, int)
assert count > 0
def test_get_usable_cpu_count_with_process_cpu_count() -> None:
"""Test get_usable_cpu_count uses process_cpu_count when available."""
# Test with process_cpu_count (Python 3.13+)
# Create a mock os module with process_cpu_count
mock_os = types.SimpleNamespace(process_cpu_count=lambda: 8, cpu_count=lambda: 4)
with patch("esphome.core.config.os", mock_os):
# When process_cpu_count exists, it should be used
count = config.get_usable_cpu_count()
assert count == 8
# Test fallback to cpu_count when process_cpu_count not available
mock_os_no_process = types.SimpleNamespace(cpu_count=lambda: 4)
with patch("esphome.core.config.os", mock_os_no_process):
count = config.get_usable_cpu_count()
assert count == 4
def test_list_target_platforms(tmp_path: Path) -> None:
"""Test _list_target_platforms returns available platforms."""
# Create mock components directory structure
@@ -2565,6 +2565,52 @@ def test_returning_lambda_no_return() -> None:
cv.returning_lambda(Lambda("int x = 5;"))
def test_returning_lambda_return_only_in_comment() -> None:
with pytest.raises(Invalid, match="return statement"):
cv.returning_lambda(Lambda("// return 5;\nint x = 5;"))
def test_returning_lambda_missing_semicolon_is_accepted() -> None:
"""A forgotten semicolon is left for the C++ compiler to report."""
assert isinstance(cv.returning_lambda(Lambda("return x")), Lambda)
@pytest.mark.parametrize(
("value", "expected"),
[
("return 5;", True),
("if (x) { return x; } return 0;", True),
("if (x) return 1; else return 0;", True),
("switch (x) { case 0: return 1; }", True),
# a semicolon means code: any return keyword counts
("return not x;", True),
("return a and b;", True),
("please return the sensor; then wait", True),
# a forgotten semicolon is still lambda source; the compiler reports it
("return id(x).state", True),
("return x", True),
("return 5", True),
("return not x", True),
# accepted: a one-word tail is indistinguishable from 'return x'
("return soon", True),
("Alert: return home", True),
("static value", False),
("no returns here", False),
("the_return_value", False),
# without a semicolon, prose is not lambda source
("please return the item", False),
("return to sender", False),
("return a and b", False),
# return only inside a comment is not a return statement
("// return 5;\nint x = 5;", False),
("/* return 5; */ int x = 5;", False),
("return 5; // done", True),
],
)
def test_looks_like_returning_lambda(value: str, expected: bool) -> None:
assert cv.looks_like_returning_lambda(value) is expected
# ---------------------------------------------------------------------------
# dimensions
# ---------------------------------------------------------------------------
+10 -10
View File
@@ -912,7 +912,7 @@ def test_prefetch_leaves_unverifiable_entries_to_the_installer(
"esphome.espidf.framework.run_command",
return_value=(True, json.dumps(entries), ""),
),
patch("esphome.espidf.framework.download_with_resume") as download,
patch("esphome.framework_helpers.download_with_resume") as download,
patch("esphome.espidf.framework.get_system_python_path", return_value="python"),
patch("esphome.framework_helpers._BatchDownloadProgress") as progress_cls,
):
@@ -935,7 +935,7 @@ def test_prefetch_all_entries_unverifiable_is_a_noop(tmp_path: Path) -> None:
"esphome.espidf.framework.run_command",
return_value=(True, json.dumps(entries), ""),
),
patch("esphome.espidf.framework.download_with_resume") as download,
patch("esphome.framework_helpers.download_with_resume") as download,
patch("esphome.espidf.framework.get_system_python_path", return_value="python"),
):
_prefetch_idf_tool_archives(tmp_path, "esp32", ["required"], None)
@@ -953,7 +953,7 @@ def test_prefetch_dedupes_entries_by_dest(tmp_path: Path) -> None:
"esphome.espidf.framework.run_command",
return_value=(True, json.dumps(entries), ""),
),
patch("esphome.espidf.framework.download_with_resume") as download,
patch("esphome.framework_helpers.download_with_resume") as download,
patch("esphome.espidf.framework.get_system_python_path", return_value="python"),
patch("esphome.framework_helpers._BatchDownloadProgress"),
):
@@ -968,7 +968,7 @@ def test_prefetch_downloads_each_archive_with_resume(tmp_path: Path) -> None:
"esphome.espidf.framework.run_command",
return_value=(True, _PREFETCH_JSON, ""),
),
patch("esphome.espidf.framework.download_with_resume") as download,
patch("esphome.framework_helpers.download_with_resume") as download,
patch("esphome.espidf.framework.get_system_python_path", return_value="python"),
patch("esphome.framework_helpers._BatchDownloadProgress") as progress_cls,
):
@@ -1012,7 +1012,7 @@ def test_prefetch_downloads_archives_concurrently(tmp_path: Path) -> None:
"esphome.espidf.framework.run_command",
return_value=(True, json.dumps(entries), ""),
),
patch("esphome.espidf.framework.download_with_resume") as download,
patch("esphome.framework_helpers.download_with_resume") as download,
patch("esphome.espidf.framework.get_system_python_path", return_value="python"),
patch(
"esphome.framework_helpers.ThreadPoolExecutor", wraps=ThreadPoolExecutor
@@ -1033,7 +1033,7 @@ def test_prefetch_skips_already_downloaded_archives(tmp_path: Path) -> None:
"esphome.espidf.framework.run_command",
return_value=(True, _PREFETCH_JSON, ""),
),
patch("esphome.espidf.framework.download_with_resume") as download,
patch("esphome.framework_helpers.download_with_resume") as download,
patch("esphome.espidf.framework.get_system_python_path", return_value="python"),
):
_prefetch_idf_tool_archives(tmp_path, "esp32", ["required"], None)
@@ -1066,7 +1066,7 @@ def test_prefetch_failures_never_raise(
with (
patch("esphome.espidf.framework.run_command", return_value=run_result),
patch(
"esphome.espidf.framework.download_with_resume",
"esphome.framework_helpers.download_with_resume",
side_effect=download_error,
),
patch("esphome.espidf.framework.get_system_python_path", return_value="python"),
@@ -1088,7 +1088,7 @@ def test_prefetch_total_failure_logs_error(
return_value=(True, _PREFETCH_JSON, ""),
),
patch(
"esphome.espidf.framework.download_with_resume",
"esphome.framework_helpers.download_with_resume",
side_effect=OSError("proxy refuses everything"),
),
patch("esphome.espidf.framework.get_system_python_path", return_value="python"),
@@ -1113,7 +1113,7 @@ def test_prefetch_one_failed_archive_does_not_stop_the_rest(
return_value=(True, _PREFETCH_JSON, ""),
),
patch(
"esphome.espidf.framework.download_with_resume",
"esphome.framework_helpers.download_with_resume",
side_effect=_fail_cmake_download,
) as download,
patch("esphome.espidf.framework.get_system_python_path", return_value="python"),
@@ -1134,7 +1134,7 @@ def test_prefetch_finishes_progress_bar_and_cancels_queue(tmp_path: Path) -> Non
"esphome.espidf.framework.run_command",
return_value=(True, _PREFETCH_JSON, ""),
),
patch("esphome.espidf.framework.download_with_resume"),
patch("esphome.framework_helpers.download_with_resume"),
patch("esphome.espidf.framework.get_system_python_path", return_value="python"),
patch("esphome.framework_helpers._BatchDownloadProgress") as progress_cls,
patch("esphome.framework_helpers.ThreadPoolExecutor") as pool_cls,
@@ -2280,6 +2280,32 @@ class TestGetProjectCxxCompileFlags:
assert get_project_cxx_compile_flags() == []
def test_resume_fetch_job_threads_tracker(tmp_path: Path) -> None:
"""The batch runner passes the tracker positionally; the shared adapter
must deliver it as download_with_resume's progress keyword."""
from esphome.framework_helpers import resume_fetch_job
with patch("esphome.framework_helpers.download_with_resume") as mock_download:
fetch = resume_fetch_job("https://x/a.zip", tmp_path / "a", sha256="ff", size=9)
tracker = lambda done: None # noqa: E731
fetch(tracker)
mock_download.assert_called_once_with(
"https://x/a.zip", tmp_path / "a", progress=tracker, sha256="ff", size=9
)
def test_warn_prefetch_failures_names_each_failure(
caplog: pytest.LogCaptureFixture,
) -> None:
"""The shared failure loop warns per job with the failure reason."""
from esphome.framework_helpers import warn_prefetch_failures
warn_prefetch_failures([("toolchain-x@1", OSError("down"))])
assert "Could not prefetch toolchain-x@1: down" in caplog.text
warn_prefetch_failures([("lib", OSError("gone"))], "Prefetch of %s failed: %s")
assert "Prefetch of lib failed: gone" in caplog.text
@pytest.mark.parametrize(
("platform", "input_path", "expected"),
[
@@ -2312,3 +2338,18 @@ def test_strip_win_long_path_prefix(
r"""``\\?\`` and ``\\?\UNC\`` prefixes are stripped only on win32."""
with patch("esphome.framework_helpers.sys.platform", platform):
assert framework_helpers.strip_win_long_path_prefix(input_path) == expected
def test_discard_partial_download_logs_undeletable(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""An unremovable staging file leaves a debug trace; the caller's
cache is never pruned, so silence would hide unbounded growth."""
dest = tmp_path / "archive"
dest.write_bytes(b"stale")
with (
patch.object(Path, "unlink", side_effect=OSError("busy")),
caplog.at_level(logging.DEBUG),
):
framework_helpers.discard_partial_download(dest)
assert "Could not remove" in caplog.text
+24
View File
@@ -4,6 +4,7 @@ import os
from pathlib import Path
import socket
import stat
import types
from unittest.mock import MagicMock, patch
from aioesphomeapi.host_resolver import AddrInfo, IPv4Sockaddr, IPv6Sockaddr
@@ -1154,3 +1155,26 @@ def test_progressbar_interrupt_keeps_finished_bar_done(monkeypatch) -> None:
def test_format_duration(seconds: float, expected: str) -> None:
"""Test that durations are rendered as short human-readable strings."""
assert helpers.format_duration(seconds) == expected
def test_get_usable_cpu_count() -> None:
"""Returns a positive int on the real host."""
count = helpers.get_usable_cpu_count()
assert isinstance(count, int)
assert count > 0
def test_get_usable_cpu_count_sources() -> None:
"""Prefers process_cpu_count, falls back to cpu_count, degrades to 1."""
mock_os = types.SimpleNamespace(process_cpu_count=lambda: 8, cpu_count=lambda: 4)
with patch("esphome.helpers.os", mock_os):
assert helpers.get_usable_cpu_count() == 8
mock_os_no_process = types.SimpleNamespace(cpu_count=lambda: 4)
with patch("esphome.helpers.os", mock_os_no_process):
assert helpers.get_usable_cpu_count() == 4
# An undeterminable count degrades to one worker, never zero
mock_os_unknown = types.SimpleNamespace(cpu_count=lambda: None)
with patch("esphome.helpers.os", mock_os_unknown):
assert helpers.get_usable_cpu_count() == 1
File diff suppressed because it is too large Load Diff
+10 -4
View File
@@ -932,8 +932,13 @@ def test_run_compile(setup_core: Path, mock_run_platformio_cli_run: Mock) -> Non
config = {CONF_ESPHOME: {CONF_COMPILE_PROCESS_LIMIT: 4}}
mock_run_platformio_cli_run.return_value = 0
toolchain.run_compile(config, verbose=True)
with patch(
"esphome.platformio.prefetch.prefetch_platformio_packages"
) as mock_prefetch:
toolchain.run_compile(config, verbose=True)
# The only wiring of the prefetch into a build lives here
mock_prefetch.assert_called_once_with()
mock_run_platformio_cli_run.assert_called_once_with(config, True, "-j4")
@@ -947,7 +952,8 @@ def test_run_compile_without_process_limit(
config = {CONF_ESPHOME: {}}
mock_run_platformio_cli_run.return_value = 0
toolchain.run_compile(config, verbose=False)
with patch("esphome.platformio.prefetch.prefetch_platformio_packages"):
toolchain.run_compile(config, verbose=False)
mock_run_platformio_cli_run.assert_called_once_with(config, False)
@@ -1677,8 +1683,8 @@ def pio_core_dir(tmp_path: Path) -> Path:
def test_current_python_minor_matches_running_interpreter() -> None:
"""_current_python_minor returns major.minor of the running interpreter."""
assert toolchain._current_python_minor() == _CURRENT_MINOR
"""current_python_minor returns major.minor of the running interpreter."""
assert toolchain.current_python_minor() == _CURRENT_MINOR
def test_pio_stamp_round_trip(tmp_path: Path) -> None: