mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[ci] Enforce list form for platform domains in test fixtures (#17869)
This commit is contained in:
@@ -117,6 +117,7 @@ jobs:
|
|||||||
script/generate-esp32-boards.py --check
|
script/generate-esp32-boards.py --check
|
||||||
script/generate-rp2-boards.py --check
|
script/generate-rp2-boards.py --check
|
||||||
script/ci_check_duplicate_test_ids.py
|
script/ci_check_duplicate_test_ids.py
|
||||||
|
script/ci_check_test_fixture_list_form.py
|
||||||
|
|
||||||
import-time:
|
import-time:
|
||||||
name: Check import esphome.__main__ time
|
name: Check import esphome.__main__ time
|
||||||
|
|||||||
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())
|
||||||
@@ -21,7 +21,7 @@ esp32:
|
|||||||
disable_fatfs: true
|
disable_fatfs: true
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
platform: esphome
|
- platform: esphome
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
ssid: MySSID
|
ssid: MySSID
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ sensor:
|
|||||||
id: espnow_temp_sensor
|
id: espnow_temp_sensor
|
||||||
|
|
||||||
- platform: packet_transport
|
- platform: packet_transport
|
||||||
|
transport_id: transport1
|
||||||
provider: test-provider
|
provider: test-provider
|
||||||
remote_id: espnow_temp_sensor
|
remote_id: espnow_temp_sensor
|
||||||
id: remote_temp
|
id: remote_temp
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ udp:
|
|||||||
addresses: ["239.0.60.53"]
|
addresses: ["239.0.60.53"]
|
||||||
|
|
||||||
packet_transport:
|
packet_transport:
|
||||||
platform: udp
|
- platform: udp
|
||||||
|
id: transport_udp
|
||||||
update_interval: 5s
|
update_interval: 5s
|
||||||
encryption: "our key goes here"
|
encryption: "our key goes here"
|
||||||
rolling_code_enable: true
|
rolling_code_enable: true
|
||||||
@@ -28,15 +29,18 @@ sensor:
|
|||||||
- platform: template
|
- platform: template
|
||||||
id: sensor_id1
|
id: sensor_id1
|
||||||
- platform: packet_transport
|
- platform: packet_transport
|
||||||
|
transport_id: transport_udp
|
||||||
provider: some-device-name
|
provider: some-device-name
|
||||||
id: our_id
|
id: our_id
|
||||||
remote_id: some_sensor_id
|
remote_id: some_sensor_id
|
||||||
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
- platform: packet_transport
|
- platform: packet_transport
|
||||||
|
transport_id: transport_udp
|
||||||
provider: unencrypted-device
|
provider: unencrypted-device
|
||||||
id: other_binary_sensor_id
|
id: other_binary_sensor_id
|
||||||
- platform: packet_transport
|
- platform: packet_transport
|
||||||
|
transport_id: transport_udp
|
||||||
provider: some-device-name
|
provider: some-device-name
|
||||||
type: status
|
type: status
|
||||||
name: Some-Device Status
|
name: Some-Device Status
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ udp:
|
|||||||
addresses: ["239.0.60.53"]
|
addresses: ["239.0.60.53"]
|
||||||
|
|
||||||
packet_transport:
|
packet_transport:
|
||||||
platform: udp
|
- platform: udp
|
||||||
|
id: transport_udp
|
||||||
update_interval: 5s
|
update_interval: 5s
|
||||||
encryption: "our key goes here"
|
encryption: "our key goes here"
|
||||||
rolling_code_enable: true
|
rolling_code_enable: true
|
||||||
@@ -24,15 +25,18 @@ sensor:
|
|||||||
- platform: template
|
- platform: template
|
||||||
id: sensor_id1
|
id: sensor_id1
|
||||||
- platform: packet_transport
|
- platform: packet_transport
|
||||||
|
transport_id: transport_udp
|
||||||
provider: some-device-name
|
provider: some-device-name
|
||||||
id: our_id
|
id: our_id
|
||||||
remote_id: some_sensor_id
|
remote_id: some_sensor_id
|
||||||
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
- platform: packet_transport
|
- platform: packet_transport
|
||||||
|
transport_id: transport_udp
|
||||||
provider: unencrypted-device
|
provider: unencrypted-device
|
||||||
id: other_binary_sensor_id
|
id: other_binary_sensor_id
|
||||||
- platform: packet_transport
|
- platform: packet_transport
|
||||||
|
transport_id: transport_udp
|
||||||
provider: some-device-name
|
provider: some-device-name
|
||||||
type: status
|
type: status
|
||||||
name: Some-Device Status
|
name: Some-Device Status
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ udp:
|
|||||||
addresses: ["239.0.60.53"]
|
addresses: ["239.0.60.53"]
|
||||||
|
|
||||||
time:
|
time:
|
||||||
platform: host
|
- platform: host
|
||||||
|
|
||||||
syslog:
|
syslog:
|
||||||
port: 514
|
port: 514
|
||||||
|
|||||||
Reference in New Issue
Block a user