mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 15:27:33 +00:00
Apply simplify pass: key shared builds off the fixture source, hoist the cache root, dedup helpers
This commit is contained in:
@@ -11,7 +11,6 @@ import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import socket
|
||||
@@ -67,6 +66,12 @@ def pytest_configure(config: pytest.Config) -> None:
|
||||
)
|
||||
|
||||
|
||||
FIXTURES_DIR = Path(__file__).parent / "fixtures"
|
||||
|
||||
# CI caches parts of this path; keep in sync with ci.yml integration-tests.
|
||||
INTEGRATION_TESTS_ROOT = Path.home() / ".esphome-integration-tests"
|
||||
|
||||
|
||||
def _get_platformio_env(cache_dir: Path) -> dict[str, str]:
|
||||
"""Get environment variables for PlatformIO with shared cache."""
|
||||
env = os.environ.copy()
|
||||
@@ -99,8 +104,7 @@ def _get_platformio_env(cache_dir: Path) -> dict[str, str]:
|
||||
def shared_platformio_cache() -> Generator[Path]:
|
||||
"""Initialize a shared PlatformIO cache for all integration tests."""
|
||||
# Use a dedicated directory for integration tests to avoid conflicts.
|
||||
# CI caches parts of this path; keep in sync with ci.yml integration-tests.
|
||||
test_cache_dir = Path.home() / ".esphome-integration-tests"
|
||||
test_cache_dir = INTEGRATION_TESTS_ROOT
|
||||
cache_dir = test_cache_dir / "platformio"
|
||||
|
||||
# Use a lock file in the home directory to ensure only one process initializes the cache
|
||||
@@ -195,15 +199,14 @@ def unused_tcp_port(reserved_tcp_port: tuple[int, socket.socket]) -> int:
|
||||
@pytest_asyncio.fixture
|
||||
async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> str:
|
||||
"""Load YAML configuration based on test name."""
|
||||
marker = request.node.get_closest_marker("shared_yaml")
|
||||
if marker is not None:
|
||||
base_name = marker.args[0]
|
||||
else:
|
||||
# Base test name: test_ prefix and any parametrization stripped
|
||||
base_name = request.node.name.replace("test_", "").partition("[")[0]
|
||||
# Base test name: test_ prefix and any parametrization stripped
|
||||
base_name = (
|
||||
_shared_yaml_name(request)
|
||||
or request.node.name.replace("test_", "").partition("[")[0]
|
||||
)
|
||||
|
||||
# Load the fixture file
|
||||
fixture_path = Path(__file__).parent / "fixtures" / f"{base_name}.yaml"
|
||||
fixture_path = FIXTURES_DIR / f"{base_name}.yaml"
|
||||
if not fixture_path.exists():
|
||||
raise FileNotFoundError(f"Fixture file not found: {fixture_path}")
|
||||
|
||||
@@ -260,16 +263,21 @@ async def write_yaml_config(
|
||||
yield _write_config
|
||||
|
||||
|
||||
SHARED_BUILDS_ROOT = Path.home() / ".esphome-integration-tests" / "builds"
|
||||
SHARED_BUILDS_ROOT = INTEGRATION_TESTS_ROOT / "builds"
|
||||
|
||||
_API_PORT_LINE_RE = re.compile(r"^(\s*port:) \d+$", re.MULTILINE)
|
||||
# ELF path per shared build dir; constant once compiled, so resolve it only once
|
||||
_shared_elf_paths: dict[Path, Path] = {}
|
||||
|
||||
|
||||
def _shared_build_key(yaml_content: str) -> str:
|
||||
"""Key shared build dirs by the config with the injected api port normalized."""
|
||||
return hashlib.sha256(
|
||||
_API_PORT_LINE_RE.sub(r"\1 0", yaml_content).encode()
|
||||
).hexdigest()[:16]
|
||||
def _shared_yaml_name(request: pytest.FixtureRequest) -> str | None:
|
||||
"""Name passed to the shared_yaml marker, or None when unmarked."""
|
||||
marker = request.node.get_closest_marker("shared_yaml")
|
||||
return marker.args[0] if marker is not None else None
|
||||
|
||||
|
||||
def _shared_build_key(name: str) -> str:
|
||||
"""Key shared build dirs by the fixture source, before per-test injections."""
|
||||
return hashlib.sha256((FIXTURES_DIR / f"{name}.yaml").read_bytes()).hexdigest()[:16]
|
||||
|
||||
|
||||
async def _run_esphome_compile(
|
||||
@@ -321,7 +329,10 @@ def _resolve_compiled_binary(config_path: Path) -> Path:
|
||||
if config is None:
|
||||
raise RuntimeError(f"Failed to read config from {config_path}")
|
||||
idedata = get_idedata(config)
|
||||
return Path(idedata.firmware_elf_path)
|
||||
binary_path = Path(idedata.firmware_elf_path)
|
||||
if not binary_path.exists():
|
||||
raise RuntimeError(f"Compiled binary not found at {binary_path}")
|
||||
return binary_path
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
@@ -338,24 +349,20 @@ async def compile_esphome(
|
||||
env = _get_platformio_env(shared_platformio_cache)
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
marker = request.node.get_closest_marker("shared_yaml")
|
||||
if marker is None:
|
||||
name = _shared_yaml_name(request)
|
||||
if name is None:
|
||||
await _run_esphome_compile(config_path, integration_test_dir, env)
|
||||
binary_path = await loop.run_in_executor(
|
||||
return await loop.run_in_executor(
|
||||
None, _resolve_compiled_binary, config_path
|
||||
)
|
||||
if not binary_path.exists():
|
||||
raise RuntimeError(f"Compiled binary not found at {binary_path}")
|
||||
return binary_path
|
||||
|
||||
# Shared fixture: build in a hash-keyed dir so tests sharing a config
|
||||
# pay one full compile and later only a main.cpp (port) rebuild + relink
|
||||
name = marker.args[0]
|
||||
content = await loop.run_in_executor(None, config_path.read_text)
|
||||
shared_dir = SHARED_BUILDS_ROOT / f"{name}-{_shared_build_key(content)}"
|
||||
shared_dir = SHARED_BUILDS_ROOT / f"{name}-{_shared_build_key(name)}"
|
||||
shared_dir.mkdir(parents=True, exist_ok=True)
|
||||
shared_config = shared_dir / f"{name}.yaml"
|
||||
private_binary = integration_test_dir / f"{name}.elf"
|
||||
content = await loop.run_in_executor(None, config_path.read_text)
|
||||
# flock serializes concurrent xdist workers; closing the fd releases it
|
||||
with (shared_dir / ".lock").open("w") as lock_file:
|
||||
await loop.run_in_executor(
|
||||
@@ -363,11 +370,11 @@ async def compile_esphome(
|
||||
)
|
||||
await loop.run_in_executor(None, shared_config.write_text, content)
|
||||
await _run_esphome_compile(shared_config, shared_dir, env)
|
||||
built = await loop.run_in_executor(
|
||||
None, _resolve_compiled_binary, shared_config
|
||||
)
|
||||
if not built.exists():
|
||||
raise RuntimeError(f"Compiled binary not found at {built}")
|
||||
if (built := _shared_elf_paths.get(shared_dir)) is None:
|
||||
built = await loop.run_in_executor(
|
||||
None, _resolve_compiled_binary, shared_config
|
||||
)
|
||||
_shared_elf_paths[shared_dir] = built
|
||||
# Copy out before unlocking: another worker may relink firmware.elf
|
||||
# while this test is still running its private copy
|
||||
await loop.run_in_executor(None, shutil.copy2, built, private_binary)
|
||||
|
||||
@@ -17,9 +17,8 @@ uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
# Shared loopback fixture for the custom_pdu, register_offset, lambda_write,
|
||||
# lambda_invert and deprecated_write_buffer tests; register spaces are disjoint
|
||||
# so each test only observes its own entities.
|
||||
# Shared loopback fixture (see the shared_yaml markers in the test file);
|
||||
# register spaces are disjoint so each test only observes its own entities.
|
||||
uart_mock:
|
||||
- id: virtual_uart_server
|
||||
baud_rate: 9600
|
||||
@@ -101,7 +100,6 @@ modbus_server:
|
||||
- address: 0x13
|
||||
value_type: U_WORD
|
||||
read_lambda: return id(reg13);
|
||||
write_lambda: id(reg13) = x; return true;
|
||||
- address: 0x30
|
||||
value_type: U_WORD
|
||||
read_lambda: return id(reg30);
|
||||
|
||||
@@ -17,17 +17,15 @@ uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
# Shared 3-bus mesh for the server_controller, server_controller_multiple
|
||||
# and client_read_write tests: two server hubs and one client hub, every bus
|
||||
# forwarding to both others. Servers: addr 1 = typed read-only registers,
|
||||
# addr 5 = client_read_write's server, addr 2/3 on the second server hub.
|
||||
# Shared 3-bus mesh (see the shared_yaml markers in the test file): two
|
||||
# server hubs and one client hub, every bus forwarding to both others.
|
||||
# Servers: addr 1 = typed read-only registers, addr 5 = the read/write 0x17
|
||||
# target, addr 2/3 on the second server hub.
|
||||
# auto_start on every bus: the controller polls at boot, so the forwarding
|
||||
# must already be live or early requests are lost and generate warnings.
|
||||
uart_mock:
|
||||
- id: virtual_uart_server
|
||||
baud_rate: 9600
|
||||
# auto_start must be true for loopback fixtures: the modbus controller
|
||||
# polls on its update_interval immediately at boot, so the uart_mock
|
||||
# forwarding must already be active or early requests are lost and
|
||||
# generate modbus warnings.
|
||||
auto_start: true
|
||||
debug:
|
||||
on_tx:
|
||||
@@ -40,7 +38,7 @@ uart_mock:
|
||||
data: !lambda return data;
|
||||
- id: virtual_uart_server_2
|
||||
baud_rate: 9600
|
||||
auto_start: true # See comment on virtual_uart_server above
|
||||
auto_start: true
|
||||
debug:
|
||||
on_tx:
|
||||
- then:
|
||||
@@ -52,7 +50,7 @@ uart_mock:
|
||||
data: !lambda return data;
|
||||
- id: virtual_uart_controller
|
||||
baud_rate: 9600
|
||||
auto_start: true # See comment on virtual_uart_server above
|
||||
auto_start: true
|
||||
debug:
|
||||
on_tx:
|
||||
- then:
|
||||
@@ -145,13 +143,11 @@ modbus_server:
|
||||
modbus_id: virtual_modbus_server
|
||||
id: modbus_server_5
|
||||
registers:
|
||||
# Writable + readable register: the read publishes what it returns, so the test can confirm the
|
||||
# write half of the 0x17 ran before the read half (Modbus 6.17).
|
||||
# Writable + readable register: srv_write_1 plus the client's read-back
|
||||
# confirm the write half of the 0x17 ran before the read half (Modbus 6.17).
|
||||
- address: 0x01
|
||||
value_type: U_WORD
|
||||
read_lambda: |-
|
||||
id(srv_read_1).publish_state(id(stored_1));
|
||||
return id(stored_1);
|
||||
read_lambda: return id(stored_1);
|
||||
write_lambda: |-
|
||||
id(stored_1) = x;
|
||||
id(srv_write_1).publish_state(x);
|
||||
@@ -280,9 +276,6 @@ sensor:
|
||||
- platform: template
|
||||
name: "srv_write_1"
|
||||
id: srv_write_1
|
||||
- platform: template
|
||||
name: "srv_read_1"
|
||||
id: srv_read_1
|
||||
- platform: template
|
||||
name: "client_read_0"
|
||||
id: client_read_0
|
||||
|
||||
@@ -17,8 +17,8 @@ uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
# Shared server-role fixture for the server and server_read_write tests; the
|
||||
# injections concatenate and each test waits only on its own sensors.
|
||||
# Shared server-role fixture (see the shared_yaml markers in the test file);
|
||||
# the injections concatenate and each test waits only on its own sensors.
|
||||
uart_mock:
|
||||
- id: virtual_uart_dev
|
||||
baud_rate: 9600
|
||||
|
||||
@@ -233,8 +233,8 @@ async def test_uart_mock_modbus_server_read_write(
|
||||
"rw_write_1": 4660, # 0x1234 written to reg 0x0001
|
||||
"rw_read_1": 4660, # reg 0x0001 reads back the just-written value
|
||||
"rw_read_2": 170, # 0x00AA read from reg 0x0002 in the same request
|
||||
"rw_write_3": 22136, # 0x5678 written to reg 0x0003
|
||||
"rw_read_3": 22136, # reg 0x0003 reads back the just-written value
|
||||
"rw_write_3": 22136, # 0x5678 written to reg 0x0006
|
||||
"rw_read_3": 22136, # reg 0x0006 reads back the just-written value
|
||||
}
|
||||
)
|
||||
|
||||
@@ -953,9 +953,7 @@ async def test_uart_mock_modbus_client_read_write(
|
||||
"""
|
||||
line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback()
|
||||
|
||||
tracker = SensorTracker(
|
||||
["srv_write_1", "srv_read_1", "client_read_0", "client_read_1"]
|
||||
)
|
||||
tracker = SensorTracker(["srv_write_1", "client_read_0", "client_read_1"])
|
||||
futures = tracker.expect_all(
|
||||
{
|
||||
"srv_write_1": 4660, # server wrote 0x1234 to reg 0x0001
|
||||
|
||||
Reference in New Issue
Block a user