mirror of
https://github.com/esphome/esphome.git
synced 2026-09-30 08:20:21 +00:00
Merge branch 'state-publish-logv' into integration
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user