From 12238eec3bede436dc833f6fd01aa6a66676d2a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Aug 2026 13:57:15 -0500 Subject: [PATCH] Log prefetch failures after the bar, assert on the shared progress instance --- esphome/espidf/framework.py | 12 +++++++++--- tests/unit_tests/test_espidf_framework.py | 8 ++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/esphome/espidf/framework.py b/esphome/espidf/framework.py index 95d168c85c..ee46364abf 100644 --- a/esphome/espidf/framework.py +++ b/esphome/espidf/framework.py @@ -747,12 +747,16 @@ def _prefetch_idf_tool_archives( len(entries), ", ".join(entry["name"] for entry in entries), ) - # tools.json always carries sizes; should one be missing the bar - # could not be trusted, so draw none rather than a wrong one. + # tools.json always carries sizes; should one be missing the combined + # bar could not be trusted, so show no bar at all (per-file bars from + # several threads would interleave) rather than a wrong one. sizes = [entry["size"] for entry in entries] progress = BatchDownloadProgress( "Downloading ESP-IDF tools", sum(sizes) if all(sizes) else 0 ) + # Reported after the bar is done so the warnings do not land on + # its row; list.append is atomic under the GIL. + failures: list[tuple[str, Exception]] = [] def _download(entry: dict) -> None: tracker = progress.tracker() @@ -768,7 +772,7 @@ def _prefetch_idf_tool_archives( # Keep prefetching the remaining archives; the installer # will retry this one itself (without resume). tracker(0) - _LOGGER.warning("Could not prefetch %s: %s", entry["name"], e) + failures.append((entry["name"], e)) ex = ThreadPoolExecutor(max_workers=min(_PREFETCH_WORKERS, len(entries))) try: @@ -779,6 +783,8 @@ def _prefetch_idf_tool_archives( # all before the process can exit; in-flight ones still finish. ex.shutdown(wait=True, cancel_futures=True) progress.done() + for name, e in failures: + _LOGGER.warning("Could not prefetch %s: %s", name, e) except Exception as e: # noqa: BLE001 # pylint: disable=broad-exception-caught # The installer downloads anything missing itself; never let the # prefetch become a new way for the install to fail. diff --git a/tests/unit_tests/test_espidf_framework.py b/tests/unit_tests/test_espidf_framework.py index f06190fee7..a453f6eece 100644 --- a/tests/unit_tests/test_espidf_framework.py +++ b/tests/unit_tests/test_espidf_framework.py @@ -896,6 +896,7 @@ def test_prefetch_downloads_each_archive_with_resume(tmp_path: Path) -> None: ), patch("esphome.espidf.framework.download_with_resume") as download, patch("esphome.espidf.framework.get_system_python_path", return_value="python"), + patch("esphome.espidf.framework.BatchDownloadProgress") as progress_cls, ): _prefetch_idf_tool_archives(tmp_path, "esp32", ["required"], None) @@ -910,10 +911,9 @@ def test_prefetch_downloads_each_archive_with_resume(tmp_path: Path) -> None: assert kwargs["sha256"] == "ab" * 32 assert kwargs["size"] == 123 # every archive reports into the one combined progress bar - assert all( - kw["progress"].__qualname__.startswith("BatchDownloadProgress.tracker") - for kw in calls.values() - ) + progress_cls.assert_called_once_with("Downloading ESP-IDF tools", 123 + 45) + tracker = progress_cls.return_value.tracker.return_value + assert all(kw["progress"] is tracker for kw in calls.values()) def test_prefetch_downloads_archives_concurrently(tmp_path: Path) -> None: