Merge branch 'state-publish-logv' into integration

This commit is contained in:
J. Nick Koston
2026-03-24 22:26:09 -10:00
38 changed files with 372 additions and 164 deletions
+54 -2
View File
@@ -593,8 +593,6 @@ static time_t esptime_recalc_local(int year, int month, int day, int hour, int m
t.hour = hour;
t.minute = min;
t.second = sec;
t.day_of_week = 1; // Placeholder for fields_in_range()
t.day_of_year = 1;
t.recalc_timestamp_local();
return t.timestamp;
}
@@ -750,6 +748,60 @@ TEST(RecalcTimestampLocal, NonDefaultTransitionTime) {
EXPECT_EQ(esp_result, libc_result);
}
TEST(RecalcTimestampLocal, MinimalFieldsWithoutDayOfWeekOrYear) {
// Regression test for issue #15115: DateTimeEntity::state_as_esptime() constructs
// an ESPTime with only year/month/day/hour/minute/second set (no day_of_week or
// day_of_year). recalc_timestamp_local() must work without those fields.
const char *tz_str = "CET-1CEST,M3.5.0,M10.5.0";
setenv("TZ", tz_str, 1);
tzset();
time::ParsedTimezone tz{};
ASSERT_TRUE(parse_posix_tz(tz_str, tz));
set_global_tz(tz);
// Construct ESPTime with only date/time fields (like state_as_esptime does)
ESPTime t{};
t.year = 2026;
t.month = 3;
t.day_of_month = 20;
t.hour = 23;
t.minute = 14;
t.second = 55;
// day_of_week and day_of_year are deliberately left as 0
t.recalc_timestamp_local();
// Must NOT return -1 (the bug: fields_in_range() rejected valid times)
EXPECT_NE(t.timestamp, -1);
// Verify against libc
time_t libc_result = libc_mktime(2026, 3, 20, 23, 14, 55);
EXPECT_EQ(t.timestamp, libc_result);
}
TEST(RecalcTimestampLocal, MinimalFieldsNoDST) {
// Same test but with a timezone that has no DST
const char *tz_str = "IST-5:30";
setenv("TZ", tz_str, 1);
tzset();
time::ParsedTimezone tz{};
ASSERT_TRUE(parse_posix_tz(tz_str, tz));
set_global_tz(tz);
ESPTime t{};
t.year = 2026;
t.month = 3;
t.day_of_month = 23;
t.hour = 10;
t.minute = 0;
t.second = 0;
t.recalc_timestamp_local();
EXPECT_NE(t.timestamp, -1);
time_t libc_result = libc_mktime(2026, 3, 23, 10, 0, 0);
EXPECT_EQ(t.timestamp, libc_result);
}
TEST(RecalcTimestampLocal, YearBoundaryDST) {
// Test southern hemisphere DST across year boundary
// Australia/Sydney: DST active from October to April (spans Jan 1)
+124
View File
@@ -867,6 +867,130 @@ def test_clean_all_with_yaml_file(
assert str(build_dir) in caplog.text
@patch("esphome.writer.CORE")
def test_clean_all_with_yaml_build_path(
mock_core: MagicMock,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test clean_all cleans absolute build_path specified in YAML config."""
config_dir = tmp_path / "config"
config_dir.mkdir()
# Create an absolute custom build path directory with contents
custom_build = tmp_path / "custom_build"
custom_build.mkdir()
(custom_build / "firmware.bin").write_text("x")
sub = custom_build / "subdir"
sub.mkdir()
(sub / "file.txt").write_text("x")
yaml_file = config_dir / "test.yaml"
# Absolute build_path: data_dir / absolute = absolute (Python Path behavior)
yaml_file.write_text(f"esphome:\n name: test\n build_path: {custom_build}\n")
# Also create the normal .esphome dir
build_dir = config_dir / ".esphome"
build_dir.mkdir()
(build_dir / "dummy.txt").write_text("x")
from esphome.writer import clean_all
with caplog.at_level("INFO"):
clean_all([str(yaml_file)])
# Both .esphome and custom build_path should be cleaned
assert build_dir.exists()
assert not (build_dir / "dummy.txt").exists()
assert custom_build.exists()
assert not (custom_build / "firmware.bin").exists()
assert not sub.exists()
@patch("esphome.writer.CORE")
def test_clean_all_with_yaml_parse_error(
mock_core: MagicMock,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test clean_all still cleans .esphome when YAML parse fails."""
config_dir = tmp_path / "config"
config_dir.mkdir()
yaml_file = config_dir / "test.yaml"
yaml_file.write_text("invalid: yaml: content: [")
build_dir = config_dir / ".esphome"
build_dir.mkdir()
(build_dir / "dummy.txt").write_text("x")
from esphome.writer import clean_all
with caplog.at_level("INFO"):
clean_all([str(yaml_file)])
# .esphome should still be cleaned despite YAML parse failure
assert build_dir.exists()
assert not (build_dir / "dummy.txt").exists()
@patch("esphome.writer.CORE")
def test_clean_all_with_env_build_path(
mock_core: MagicMock,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test clean_all cleans ESPHOME_BUILD_PATH directory."""
config_dir = tmp_path / "config"
config_dir.mkdir()
build_dir = config_dir / ".esphome"
build_dir.mkdir()
(build_dir / "dummy.txt").write_text("x")
# Create env build path directory
env_build = tmp_path / "env_build"
env_build.mkdir()
(env_build / "firmware.bin").write_text("x")
from esphome.writer import clean_all
with (
caplog.at_level("INFO"),
patch.dict(os.environ, {"ESPHOME_BUILD_PATH": str(env_build)}),
):
clean_all([str(config_dir)])
# Both should be cleaned
assert not (build_dir / "dummy.txt").exists()
assert env_build.exists()
assert not (env_build / "firmware.bin").exists()
@patch("esphome.writer.CORE")
def test_clean_all_ignores_empty_env_vars(
mock_core: MagicMock,
tmp_path: Path,
) -> None:
"""Test clean_all ignores empty ESPHOME_BUILD_PATH/ESPHOME_DATA_DIR."""
config_dir = tmp_path / "config"
config_dir.mkdir()
# Create a file in cwd that must NOT be cleaned
marker = tmp_path / "important.txt"
marker.write_text("do not delete")
from esphome.writer import clean_all
with patch.dict(
os.environ,
{"ESPHOME_BUILD_PATH": "", "ESPHOME_DATA_DIR": ""},
):
clean_all([str(config_dir)])
# Empty env vars must not cause cwd to be cleaned
assert marker.exists()
@patch("esphome.writer.CORE")
def test_clean_all(
mock_core: MagicMock,