Add build info to image (#12425)
Co-authored-by: J. Nick Koston <nick+github@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -4,9 +4,11 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
import time
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
@@ -22,6 +24,7 @@ from esphome.__main__ import (
|
||||
command_rename,
|
||||
command_update_all,
|
||||
command_wizard,
|
||||
compile_program,
|
||||
detect_external_components,
|
||||
get_port_type,
|
||||
has_ip_address,
|
||||
@@ -2605,3 +2608,197 @@ def test_command_analyze_memory_no_idedata(
|
||||
|
||||
assert result == 1
|
||||
assert "Failed to get IDE data for memory analysis" in caplog.text
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_compile_build_info_run_compile() -> Generator[Mock]:
|
||||
"""Mock platformio_api.run_compile for build_info tests."""
|
||||
with patch("esphome.platformio_api.run_compile", return_value=0) as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_compile_build_info_get_idedata() -> Generator[Mock]:
|
||||
"""Mock platformio_api.get_idedata for build_info tests."""
|
||||
mock_idedata = MagicMock()
|
||||
with patch("esphome.platformio_api.get_idedata", return_value=mock_idedata) as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
def _setup_build_info_test(
|
||||
tmp_path: Path,
|
||||
*,
|
||||
create_firmware: bool = True,
|
||||
create_build_info: bool = True,
|
||||
build_info_content: str | None = None,
|
||||
firmware_first: bool = False,
|
||||
) -> tuple[Path, Path]:
|
||||
"""Set up build directory structure for build_info tests.
|
||||
|
||||
Args:
|
||||
tmp_path: Temporary directory path.
|
||||
create_firmware: Whether to create firmware.bin file.
|
||||
create_build_info: Whether to create build_info.json file.
|
||||
build_info_content: Custom content for build_info.json, or None for default.
|
||||
firmware_first: If True, create firmware before build_info (makes firmware older).
|
||||
|
||||
Returns:
|
||||
Tuple of (build_info_path, firmware_path).
|
||||
"""
|
||||
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device")
|
||||
|
||||
build_path = tmp_path / ".esphome" / "build" / "test_device"
|
||||
pioenvs_path = build_path / ".pioenvs" / "test_device"
|
||||
pioenvs_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
build_info_path = build_path / "build_info.json"
|
||||
firmware_path = pioenvs_path / "firmware.bin"
|
||||
|
||||
default_build_info = json.dumps(
|
||||
{
|
||||
"config_hash": 0x12345678,
|
||||
"build_time": int(time.time()),
|
||||
"build_time_str": "Dec 13 2025, 12:00:00",
|
||||
"esphome_version": "2025.1.0",
|
||||
}
|
||||
)
|
||||
|
||||
def create_build_info_file() -> None:
|
||||
if create_build_info:
|
||||
content = (
|
||||
build_info_content
|
||||
if build_info_content is not None
|
||||
else default_build_info
|
||||
)
|
||||
build_info_path.write_text(content)
|
||||
|
||||
def create_firmware_file() -> None:
|
||||
if create_firmware:
|
||||
firmware_path.write_bytes(b"fake firmware")
|
||||
|
||||
if firmware_first:
|
||||
create_firmware_file()
|
||||
time.sleep(0.01) # Ensure different timestamps
|
||||
create_build_info_file()
|
||||
else:
|
||||
create_build_info_file()
|
||||
time.sleep(0.01) # Ensure different timestamps
|
||||
create_firmware_file()
|
||||
|
||||
return build_info_path, firmware_path
|
||||
|
||||
|
||||
def test_compile_program_emits_build_info_when_firmware_rebuilt(
|
||||
tmp_path: Path,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
mock_compile_build_info_run_compile: Mock,
|
||||
mock_compile_build_info_get_idedata: Mock,
|
||||
) -> None:
|
||||
"""Test that compile_program logs build_info when firmware is rebuilt."""
|
||||
_setup_build_info_test(tmp_path, firmware_first=False)
|
||||
|
||||
config: dict[str, Any] = {CONF_ESPHOME: {CONF_NAME: "test_device"}}
|
||||
args = MockArgs()
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
result = compile_program(args, config)
|
||||
|
||||
assert result == 0
|
||||
assert "Build Info: config_hash=0x12345678" in caplog.text
|
||||
|
||||
|
||||
def test_compile_program_no_build_info_when_firmware_not_rebuilt(
|
||||
tmp_path: Path,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
mock_compile_build_info_run_compile: Mock,
|
||||
mock_compile_build_info_get_idedata: Mock,
|
||||
) -> None:
|
||||
"""Test that compile_program doesn't log build_info when firmware wasn't rebuilt."""
|
||||
_setup_build_info_test(tmp_path, firmware_first=True)
|
||||
|
||||
config: dict[str, Any] = {CONF_ESPHOME: {CONF_NAME: "test_device"}}
|
||||
args = MockArgs()
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
result = compile_program(args, config)
|
||||
|
||||
assert result == 0
|
||||
assert "Build Info:" not in caplog.text
|
||||
|
||||
|
||||
def test_compile_program_no_build_info_when_firmware_missing(
|
||||
tmp_path: Path,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
mock_compile_build_info_run_compile: Mock,
|
||||
mock_compile_build_info_get_idedata: Mock,
|
||||
) -> None:
|
||||
"""Test that compile_program doesn't log build_info when firmware.bin doesn't exist."""
|
||||
_setup_build_info_test(tmp_path, create_firmware=False)
|
||||
|
||||
config: dict[str, Any] = {CONF_ESPHOME: {CONF_NAME: "test_device"}}
|
||||
args = MockArgs()
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
result = compile_program(args, config)
|
||||
|
||||
assert result == 0
|
||||
assert "Build Info:" not in caplog.text
|
||||
|
||||
|
||||
def test_compile_program_no_build_info_when_json_missing(
|
||||
tmp_path: Path,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
mock_compile_build_info_run_compile: Mock,
|
||||
mock_compile_build_info_get_idedata: Mock,
|
||||
) -> None:
|
||||
"""Test that compile_program doesn't log build_info when build_info.json doesn't exist."""
|
||||
_setup_build_info_test(tmp_path, create_build_info=False)
|
||||
|
||||
config: dict[str, Any] = {CONF_ESPHOME: {CONF_NAME: "test_device"}}
|
||||
args = MockArgs()
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
result = compile_program(args, config)
|
||||
|
||||
assert result == 0
|
||||
assert "Build Info:" not in caplog.text
|
||||
|
||||
|
||||
def test_compile_program_no_build_info_when_json_invalid(
|
||||
tmp_path: Path,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
mock_compile_build_info_run_compile: Mock,
|
||||
mock_compile_build_info_get_idedata: Mock,
|
||||
) -> None:
|
||||
"""Test that compile_program doesn't log build_info when build_info.json is invalid."""
|
||||
_setup_build_info_test(tmp_path, build_info_content="not valid json {{{")
|
||||
|
||||
config: dict[str, Any] = {CONF_ESPHOME: {CONF_NAME: "test_device"}}
|
||||
args = MockArgs()
|
||||
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
result = compile_program(args, config)
|
||||
|
||||
assert result == 0
|
||||
assert "Build Info:" not in caplog.text
|
||||
|
||||
|
||||
def test_compile_program_no_build_info_when_json_missing_keys(
|
||||
tmp_path: Path,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
mock_compile_build_info_run_compile: Mock,
|
||||
mock_compile_build_info_get_idedata: Mock,
|
||||
) -> None:
|
||||
"""Test that compile_program doesn't log build_info when build_info.json is missing required keys."""
|
||||
_setup_build_info_test(
|
||||
tmp_path, build_info_content=json.dumps({"build_time": 1234567890})
|
||||
)
|
||||
|
||||
config: dict[str, Any] = {CONF_ESPHOME: {CONF_NAME: "test_device"}}
|
||||
args = MockArgs()
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
result = compile_program(args, config)
|
||||
|
||||
assert result == 0
|
||||
assert "Build Info:" not in caplog.text
|
||||
|
||||
Reference in New Issue
Block a user