[core] Make config-hash independent of machine-local paths (#17523)

This commit is contained in:
J. Nick Koston
2026-07-11 14:30:47 -10:00
committed by GitHub
parent 65353006c8
commit c0636e2bf7
5 changed files with 143 additions and 9 deletions
+42
View File
@@ -1113,6 +1113,48 @@ def test_config_hash_different_for_different_configs() -> None:
assert hash1 != hash2
def test_config_hash_ignores_build_path() -> None:
"""Test that config_hash does not depend on the build_path value.
build_path embeds ESPHOME_BUILD_PATH and OS path separators, so it must
not make the hash differ between machines.
"""
CORE.reset()
CORE.config = {"esphome": {"name": "test", "build_path": "build\\test"}}
hash1 = CORE.config_hash
CORE.reset()
CORE.config = {"esphome": {"name": "test", "build_path": "/build/test"}}
hash2 = CORE.config_hash
assert hash1 == hash2
def test_config_hash_same_for_different_config_dirs(tmp_path: Path) -> None:
"""Test that Path values under the config dir hash the same everywhere.
Simulates the same project checked out at two different locations; the
absolute paths differ but the layout relative to the config dir is the
same, so the hashes must match.
"""
dir1 = tmp_path / "machine_a" / "project"
dir2 = tmp_path / "machine_b" / "somewhere" / "else"
dir1.mkdir(parents=True)
dir2.mkdir(parents=True)
CORE.reset()
CORE.config_path = dir1 / "device.yaml"
CORE.config = {"esphome": {"name": "test"}, "file": dir1 / "fonts" / "arial.ttf"}
hash1 = CORE.config_hash
CORE.reset()
CORE.config_path = dir2 / "device.yaml"
CORE.config = {"esphome": {"name": "test"}, "file": dir2 / "fonts" / "arial.ttf"}
hash2 = CORE.config_hash
assert hash1 == hash2
def test_make_app_name_cpp_no_mac_simple() -> None:
"""Test simple name without MAC suffix returns string literal."""
cpp_expr, global_decl, byte_len = make_app_name_cpp(
+2 -2
View File
@@ -167,9 +167,9 @@ def setup_core(
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform}
if tmp_path is not None:
CORE.config_path = str(tmp_path / f"{name}.yaml")
CORE.config_path = tmp_path / f"{name}.yaml"
CORE.name = name
CORE.build_path = str(tmp_path / ".esphome" / "build" / name)
CORE.build_path = tmp_path / ".esphome" / "build" / name
@pytest.fixture
+51
View File
@@ -1349,6 +1349,57 @@ def test_sensitive_str__is_a_str_subclass() -> None:
assert value == "hunter2"
def test_dump_path_without_relative_to_is_unchanged() -> None:
"""Test that Path values dump as str(path) when relative_to is not given."""
path = Path("some") / "dir" / "file.ttf"
output = yaml_util.dump({"file": path})
assert output.strip() == f"file: {path}"
def test_dump_path_relative_to_anchor_dir() -> None:
"""Test that Path values under relative_to dump as relative POSIX paths."""
anchor = Path("/config/esphome").absolute()
data = {"file": anchor / "fonts" / "arial.ttf"}
output = yaml_util.dump(data, relative_to=anchor)
assert output.strip() == "file: fonts/arial.ttf"
def test_dump_path_outside_anchor_dir_walks_up() -> None:
"""Test that Path values outside relative_to walk up with ".." segments."""
anchor = Path("/config/esphome").absolute()
outside = Path("/config/fonts/file.ttf").absolute()
output = yaml_util.dump({"file": outside}, relative_to=anchor)
assert output.strip() == "file: ../fonts/file.ttf"
def test_dump_path_with_dotdot_segments_is_normalized() -> None:
"""Test that ".." segments do not defeat relativization.
A path like /config/other/../esphome/fonts/x.ttf is under the anchor
once normalized, so it must dump as a plain relative path.
"""
anchor = Path("/config/esphome").absolute()
path = Path("/config/other/../esphome/fonts/x.ttf").absolute()
output = yaml_util.dump({"file": path}, relative_to=anchor)
assert output.strip() == "file: fonts/x.ttf"
def test_dump_path_dotdot_reference_outside_anchor() -> None:
"""Test the relative_config_path("../...") shape stays relative."""
anchor = Path("/config/esphome").absolute()
path = anchor / ".." / "shared" / "font.ttf"
output = yaml_util.dump({"file": path}, relative_to=anchor)
assert output.strip() == "file: ../shared/font.ttf"
def test_dump_relative_to_does_not_leak_between_calls() -> None:
"""Test that the relative_to flag is scoped to a single dump call."""
anchor = Path("/config/esphome").absolute()
path = anchor / "fonts" / "arial.ttf"
assert "fonts/arial.ttf" in yaml_util.dump({"file": path}, relative_to=anchor)
assert yaml_util.dump({"file": path}).strip() == f"file: {path}"
def test_dump__redacts_sensitive_str_by_default() -> None:
out = yaml_util.dump({"password": SensitiveStr("hunter2")})
assert "\\033[8mhunter2\\033[28m" in out