mirror of
https://github.com/esphome/esphome.git
synced 2026-09-29 07:50:21 +00:00
Merge remote-tracking branch 'origin/dev' into jesserockz-2026-503
This commit is contained in:
@@ -194,7 +194,7 @@ def cmd_update(args: argparse.Namespace) -> int:
|
||||
|
||||
|
||||
def cmd_har_only(args: argparse.Namespace) -> int:
|
||||
Path(args.har).write_text(run_waterfall(TARGET_MODULE))
|
||||
Path(args.har).write_text(run_waterfall(TARGET_MODULE), encoding="utf-8")
|
||||
print(f"Wrote waterfall HAR to {args.har}")
|
||||
return 0
|
||||
|
||||
|
||||
+19
-13
@@ -348,9 +348,11 @@ def lint_const_ordered(fname, content):
|
||||
(
|
||||
mi,
|
||||
1,
|
||||
f"Constant {highlight(mline)} is not ordered, please make sure all "
|
||||
f"constants are ordered. See line {mi} (should go to line {target}, "
|
||||
f"{target_text})",
|
||||
(
|
||||
f"Constant {highlight(mline)} is not ordered, please make sure all "
|
||||
f"constants are ordered. See line {mi} (should go to line {target}, "
|
||||
f"{target_text})"
|
||||
),
|
||||
)
|
||||
)
|
||||
return errs
|
||||
@@ -1005,12 +1007,14 @@ def lint_log_multiline_continuation(fname, content):
|
||||
(
|
||||
lineno,
|
||||
col,
|
||||
"Multi-line log message has a continuation line that does "
|
||||
"not start with a space. The log viewer uses leading "
|
||||
"whitespace to detect continuation lines and re-add the "
|
||||
f"log tag prefix (e.g. {highlight('[C][component:042]:')}).\n"
|
||||
"Either start the continuation with a space/indent, or "
|
||||
"split into separate ESP_LOG* calls.",
|
||||
(
|
||||
"Multi-line log message has a continuation line that does "
|
||||
"not start with a space. The log viewer uses leading "
|
||||
"whitespace to detect continuation lines and re-add the "
|
||||
f"log tag prefix (e.g. {highlight('[C][component:042]:')}).\n"
|
||||
"Either start the continuation with a space/indent, or "
|
||||
"split into separate ESP_LOG* calls."
|
||||
),
|
||||
)
|
||||
)
|
||||
return errs
|
||||
@@ -1088,10 +1092,12 @@ def lint_test_package_key_matches_bus(fname, content):
|
||||
(
|
||||
lineno,
|
||||
1,
|
||||
f"Package key {highlight(pkg_key)} does not match bus directory "
|
||||
f"{highlight(bus_dir)}. The package key must match the directory "
|
||||
f"name under tests/test_build_components/common/. "
|
||||
f"Change {highlight(pkg_key)} to {highlight(bus_dir)}.",
|
||||
(
|
||||
f"Package key {highlight(pkg_key)} does not match bus directory "
|
||||
f"{highlight(bus_dir)}. The package key must match the directory "
|
||||
f"name under tests/test_build_components/common/. "
|
||||
f"Change {highlight(pkg_key)} to {highlight(bus_dir)}."
|
||||
),
|
||||
)
|
||||
)
|
||||
return errs
|
||||
|
||||
Executable
+103
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fail when a test fixture writes a platform-list domain as a single dict.
|
||||
|
||||
Component tests are merged and built in groups in CI (see
|
||||
``script/merge_component_configs.py``). ESPHome's ``merge_config`` concatenates
|
||||
two lists, but when one side is a dict it replaces the other side wholesale
|
||||
(``esphome/config_helpers.py``). A domain such as ``one_wire:`` or ``ota:``
|
||||
written in single-dict form therefore deletes every entry other components
|
||||
contributed to that domain before it in the merge, and is itself deleted by any
|
||||
list that merges after it. The resulting failure only appears when the affected
|
||||
components land in the same group -- usually a full component matrix run on an
|
||||
unrelated PR long after the fixture was written (this is what broke the
|
||||
dallas_temp tests when ds2484 was added, see #17868).
|
||||
|
||||
This guard scans every fixture under ``tests/components/`` and rejects any
|
||||
top-level domain written as a dict with a ``platform`` key. Such a domain is by
|
||||
definition a platform list (single-dict form is only user-config sugar), so the
|
||||
fix is always to write it as a one-element list:
|
||||
|
||||
one_wire:
|
||||
- platform: gpio
|
||||
pin: 4
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from esphome.core import EsphomeError # noqa: E402
|
||||
from script.analyze_component_buses import ISOLATED_COMPONENTS # noqa: E402
|
||||
from script.merge_component_configs import load_yaml_file # noqa: E402
|
||||
|
||||
# Resolved relative to this file (not the CWD) so the scan cannot silently cover
|
||||
# nothing when run from a different directory.
|
||||
ROOT_DIR = Path(__file__).resolve().parent.parent
|
||||
TESTS_DIR = ROOT_DIR / "tests" / "components"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
offenders: list[str] = []
|
||||
parse_errors: list[str] = []
|
||||
fixtures_scanned = 0
|
||||
|
||||
for fixture in sorted(TESTS_DIR.glob("*/*.yaml")):
|
||||
# Isolated components are never merged with others, so dict form
|
||||
# cannot clobber anyone there.
|
||||
if fixture.parent.name in ISOLATED_COMPONENTS:
|
||||
continue
|
||||
try:
|
||||
data = load_yaml_file(fixture)
|
||||
except EsphomeError as err:
|
||||
parse_errors.append(f"{fixture.relative_to(ROOT_DIR)}: {err}")
|
||||
continue
|
||||
fixtures_scanned += 1
|
||||
if not isinstance(data, dict):
|
||||
continue
|
||||
for key, value in data.items():
|
||||
if isinstance(value, dict) and "platform" in value:
|
||||
offenders.append(f"{fixture.relative_to(ROOT_DIR)}: '{key}:'")
|
||||
|
||||
if offenders:
|
||||
print("Test fixtures with platform domains in single-dict form:\n")
|
||||
for line in offenders:
|
||||
print(f" - {line}")
|
||||
print(
|
||||
"\nWrite the domain as a one-element list ('- platform: ...') so "
|
||||
"grouped CI builds can merge it with other components' entries; "
|
||||
"in dict form it replaces or is replaced by their lists wholesale."
|
||||
)
|
||||
|
||||
if parse_errors:
|
||||
# A fixture we could not parse was never scanned, so the run is not a
|
||||
# clean pass even if no offenders were found among the rest.
|
||||
print(
|
||||
f"\n{len(parse_errors)} test fixture(s) could not be parsed and "
|
||||
"were not checked:"
|
||||
)
|
||||
for line in parse_errors:
|
||||
print(f" - {line}")
|
||||
|
||||
if fixtures_scanned == 0:
|
||||
# A scan that covered nothing is a false green -- the whole point of the
|
||||
# guard is defeated. Fail loudly (wrong working directory or layout change).
|
||||
print(
|
||||
f"\nERROR: scanned 0 test fixtures under {TESTS_DIR}; "
|
||||
"the guard covered nothing.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if offenders or parse_errors or fixtures_scanned == 0:
|
||||
return 1
|
||||
|
||||
print(
|
||||
f"No single-dict platform domains found ({fixtures_scanned} fixtures scanned)."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -269,7 +269,7 @@ def main() -> int:
|
||||
if args.output_build_dir and build_dir:
|
||||
build_dir_path = Path(args.output_build_dir)
|
||||
build_dir_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
build_dir_path.write_text(build_dir)
|
||||
build_dir_path.write_text(build_dir, encoding="utf-8")
|
||||
print(f"Wrote build directory to {args.output_build_dir}", file=sys.stderr)
|
||||
|
||||
# Run detailed analysis if build directory available
|
||||
|
||||
+4
-1
@@ -421,7 +421,10 @@ def _get_github_event_data() -> dict | None:
|
||||
"""
|
||||
github_event_path = os.environ.get("GITHUB_EVENT_PATH")
|
||||
if github_event_path and Path(github_event_path).exists():
|
||||
with Path(github_event_path).open() as f:
|
||||
# The event payload is UTF-8 JSON; without an explicit encoding
|
||||
# Windows decodes it as cp1252 and any non ASCII byte (an ellipsis in
|
||||
# a commit title is enough) raises UnicodeDecodeError.
|
||||
with Path(github_event_path).open(encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
return None
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"target_module": "esphome.__main__",
|
||||
"margin_pct": 20,
|
||||
"cumulative_us": 95000
|
||||
"cumulative_us": 37200
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ def _read_codspeed_version(cmake_path: Path) -> str:
|
||||
"""Extract CODSPEED_VERSION from core/CMakeLists.txt."""
|
||||
if not cmake_path.exists():
|
||||
return "0.0.0"
|
||||
for line in cmake_path.read_text().splitlines():
|
||||
for line in cmake_path.read_text(encoding="utf-8").splitlines():
|
||||
if line.startswith("set(CODSPEED_VERSION"):
|
||||
return line.split()[1].rstrip(")")
|
||||
return "0.0.0"
|
||||
|
||||
@@ -367,7 +367,7 @@ def run_esphome_test(
|
||||
output_file = build_dir / f"{component}.{test_name}.{platform_with_version}.yaml"
|
||||
|
||||
# Copy base file and substitute component test file reference
|
||||
base_content = base_file.read_text()
|
||||
base_content = base_file.read_text(encoding="utf-8")
|
||||
# Get relative path from build dir to test file
|
||||
repo_root = Path(__file__).parent.parent
|
||||
component_test_ref = f"../../{test_file.relative_to(repo_root / 'tests')}"
|
||||
@@ -524,7 +524,7 @@ def run_grouped_test(
|
||||
|
||||
# Create test file that includes merged config
|
||||
output_file = build_dir / f"test_{group_name}.{platform_with_version}.yaml"
|
||||
base_content = base_file.read_text()
|
||||
base_content = base_file.read_text(encoding="utf-8")
|
||||
merged_ref = merged_config_file.name
|
||||
output_content = base_content.replace("$component_test_file", merged_ref)
|
||||
output_file.write_text(output_content)
|
||||
|
||||
Reference in New Issue
Block a user