[core] Parse stored framework versions without importing the validation stack (#18045)

This commit is contained in:
J. Nick Koston
2026-08-03 20:59:38 -05:00
committed by GitHub
parent 11c3c06b12
commit 1b3891866c
11 changed files with 182 additions and 82 deletions
@@ -0,0 +1,52 @@
"""Run the esp32 storage fast path and report which heavy modules loaded.
Executed as a subprocess by test_lazy_imports.py: heavy module names come
in on argv, the ones found in sys.modules afterwards go out on stdout.
"""
import sys
from esphome.const import KEY_ESP32, KEY_IDF_VERSION, KEY_VARIANT
from esphome.core import CORE, Version
from esphome.storage_json import StorageJSON
storage = StorageJSON(
storage_version=1,
name="test",
friendly_name="Test",
comment=None,
esphome_version="2026.1.0",
src_version=1,
address="1.2.3.4",
web_port=None,
target_platform="ESP32S3",
build_path=None,
firmware_bin_path=None,
loaded_integrations=set(),
loaded_platforms=set(),
no_mdns=False,
framework="esp-idf",
core_platform="esp32",
area=None,
framework_version="5.3.1",
)
storage.apply_to_core()
# Fail loudly if the esp32 fast path stopped doing its work; otherwise an
# empty leak list could just mean nothing ran. Explicit exits rather than
# asserts so PYTHONOPTIMIZE in the ambient environment can't strip them.
esp32_data = CORE.data.get(KEY_ESP32, {})
if esp32_data.get(KEY_VARIANT) != "ESP32S3":
sys.exit(f"apply_to_core did not record the variant: {esp32_data!r}")
if esp32_data.get(KEY_IDF_VERSION) != Version(5, 3, 1):
sys.exit(f"apply_to_core did not parse the framework version: {esp32_data!r}")
# Any component package counts as a leak, not just the ones on the watch
# list: executing one drags in codegen/validation machinery by design.
leaked = [module for module in sys.argv[1:] if module in sys.modules]
leaked += [
module
for module in sys.modules
if module.startswith("esphome.components.") and module not in leaked
]
print(",".join(leaked))
+1 -6
View File
@@ -20,6 +20,7 @@ from esphome.const import (
CONF_ESPHOME,
CONF_NAME,
KEY_CORE,
KEY_ESP32,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
KEY_VARIANT,
@@ -130,15 +131,11 @@ def test_load_compiled_config_happy_path(fresh_cache_files: Path) -> None:
assert CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] == "esp32"
assert CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] == "arduino"
# upload_using_esptool reads get_esp32_variant() off CORE.data[KEY_ESP32].
from esphome.components.esp32.const import KEY_ESP32
assert CORE.data[KEY_ESP32][KEY_VARIANT] == "ESP32"
def test_load_compiled_config_populates_esp32_variant(tmp_path: Path) -> None:
"""ESP32 variants survive the cache fast path so esptool gets the right --chip."""
from esphome.components.esp32.const import KEY_ESP32
yaml_path = tmp_path / "lite_test.yaml"
yaml_path.write_text("esphome:\n name: lite_test\n")
CORE.config_path = yaml_path
@@ -156,8 +153,6 @@ def test_load_compiled_config_skips_esp32_block_for_other_platforms(
tmp_path: Path,
) -> None:
"""Non-esp32 targets shouldn't fabricate an esp32 data block."""
from esphome.components.esp32.const import KEY_ESP32
yaml_path = tmp_path / "lite_test.yaml"
yaml_path.write_text("esphome:\n name: lite_test\n")
CORE.config_path = yaml_path
+45
View File
@@ -14,6 +14,9 @@ test pins down *which* heavy modules must stay out entirely.
from __future__ import annotations
import importlib.util
import os
from pathlib import Path
import subprocess
import sys
@@ -30,6 +33,10 @@ HEAVY_MODULES = (
"voluptuous",
)
# Everything the storage fast path must keep out of sys.modules; the
# existence guard and the leak check must watch the same list.
FAST_PATH_HEAVY_MODULES = HEAVY_MODULES + ("esphome.components.esp32",)
def _leaked_heavy_modules(module: str) -> str:
"""Import ``module`` in a subprocess and report the heavy modules it pulled.
@@ -63,6 +70,44 @@ def test_main_module_does_not_import_heavy_modules() -> None:
)
def test_watched_heavy_modules_exist() -> None:
"""A renamed heavy module would silently disable the leak checks."""
for module in FAST_PATH_HEAVY_MODULES:
assert importlib.util.find_spec(module) is not None, (
f"{module} no longer resolves; update the heavy-module lists"
)
def test_storage_json_fast_path_does_not_import_heavy_modules(
fixture_path: Path,
) -> None:
"""``apply_to_core`` runs on the upload/logs fast path for every
platform; parsing the stored framework version must not drag in the
validation stack or the esp32 component package.
"""
script = fixture_path / "lazy_imports" / "storage_json_fast_path.py"
# Running a script file drops the cwd from sys.path, so prepend the
# repo root for the child; check=False keeps its stderr visible.
python_path = str(Path(__file__).parents[2])
if ambient := os.environ.get("PYTHONPATH"):
python_path = os.pathsep.join((python_path, ambient))
env = os.environ | {"PYTHONPATH": python_path}
result = subprocess.run(
[sys.executable, str(script), *FAST_PATH_HEAVY_MODULES],
capture_output=True,
text=True,
env=env,
check=False,
)
assert result.returncode == 0, result.stderr
leaked = result.stdout.strip()
assert not leaked, (
f"storage_json.apply_to_core pulls in heavy modules: {leaked}. "
"The upload/logs fast path skips validation; importing the "
"validation stack anyway defeats the validated-config cache."
)
def test_api_client_does_not_import_heavy_modules() -> None:
"""``esphome.api_client`` is on the logs fast path and must stay light.