mirror of
https://github.com/esphome/esphome.git
synced 2026-09-03 19:46:02 +00:00
cover
This commit is contained in:
@@ -0,0 +1,678 @@
|
||||
// Tests for the POSIX TZ parser implementation
|
||||
// This verifies our custom parser produces identical results to libc's
|
||||
// tzset()/localtime() implementation. The custom parser avoids pulling in scanf (~7.6KB).
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include "esphome/core/posix_tz.h"
|
||||
|
||||
namespace esphome::time::testing {
|
||||
|
||||
// ============================================================================
|
||||
// Basic TZ string parsing tests
|
||||
// ============================================================================
|
||||
|
||||
TEST(PosixTzParser, ParseSimpleOffsetEST5) {
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz("EST5", tz));
|
||||
EXPECT_EQ(tz.std_offset_seconds, 5 * 3600); // +5 hours (west of UTC)
|
||||
EXPECT_FALSE(tz.has_dst);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParseNegativeOffsetCET) {
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz("CET-1", tz));
|
||||
EXPECT_EQ(tz.std_offset_seconds, -1 * 3600); // -1 hour (east of UTC)
|
||||
EXPECT_FALSE(tz.has_dst);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParseExplicitPositiveOffset) {
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz("TEST+5", tz));
|
||||
EXPECT_EQ(tz.std_offset_seconds, 5 * 3600);
|
||||
EXPECT_FALSE(tz.has_dst);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParseZeroOffset) {
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz("UTC0", tz));
|
||||
EXPECT_EQ(tz.std_offset_seconds, 0);
|
||||
EXPECT_FALSE(tz.has_dst);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParseUSEasternWithDST) {
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz("EST5EDT,M3.2.0,M11.1.0", tz));
|
||||
EXPECT_EQ(tz.std_offset_seconds, 5 * 3600);
|
||||
EXPECT_EQ(tz.dst_offset_seconds, 4 * 3600); // Default: STD - 1hr
|
||||
EXPECT_TRUE(tz.has_dst);
|
||||
EXPECT_EQ(tz.dst_start.month, 3);
|
||||
EXPECT_EQ(tz.dst_start.week, 2);
|
||||
EXPECT_EQ(tz.dst_start.day_of_week, 0); // Sunday
|
||||
EXPECT_EQ(tz.dst_start.time_seconds, 2 * 3600); // Default 2:00 AM
|
||||
EXPECT_EQ(tz.dst_end.month, 11);
|
||||
EXPECT_EQ(tz.dst_end.week, 1);
|
||||
EXPECT_EQ(tz.dst_end.day_of_week, 0);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParseUSCentralWithTime) {
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz("CST6CDT,M3.2.0/2,M11.1.0/2", tz));
|
||||
EXPECT_EQ(tz.std_offset_seconds, 6 * 3600);
|
||||
EXPECT_EQ(tz.dst_offset_seconds, 5 * 3600);
|
||||
EXPECT_EQ(tz.dst_start.time_seconds, 2 * 3600); // 2:00 AM
|
||||
EXPECT_EQ(tz.dst_end.time_seconds, 2 * 3600);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParseEuropeBerlin) {
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz("CET-1CEST,M3.5.0,M10.5.0/3", tz));
|
||||
EXPECT_EQ(tz.std_offset_seconds, -1 * 3600);
|
||||
EXPECT_EQ(tz.dst_offset_seconds, -2 * 3600); // Default: STD - 1hr
|
||||
EXPECT_TRUE(tz.has_dst);
|
||||
EXPECT_EQ(tz.dst_start.month, 3);
|
||||
EXPECT_EQ(tz.dst_start.week, 5); // Last week
|
||||
EXPECT_EQ(tz.dst_end.month, 10);
|
||||
EXPECT_EQ(tz.dst_end.week, 5); // Last week
|
||||
EXPECT_EQ(tz.dst_end.time_seconds, 3 * 3600); // 3:00 AM
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParseNewZealand) {
|
||||
ParsedTimezone tz;
|
||||
// Southern hemisphere - DST starts in Sept, ends in April
|
||||
ASSERT_TRUE(parse_posix_tz("NZST-12NZDT,M9.5.0,M4.1.0/3", tz));
|
||||
EXPECT_EQ(tz.std_offset_seconds, -12 * 3600);
|
||||
EXPECT_EQ(tz.dst_offset_seconds, -13 * 3600); // Default: STD - 1hr
|
||||
EXPECT_TRUE(tz.has_dst);
|
||||
EXPECT_EQ(tz.dst_start.month, 9); // September
|
||||
EXPECT_EQ(tz.dst_end.month, 4); // April
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParseExplicitDstOffset) {
|
||||
ParsedTimezone tz;
|
||||
// Some places have non-standard DST offsets
|
||||
ASSERT_TRUE(parse_posix_tz("TEST5DST4,M3.2.0,M11.1.0", tz));
|
||||
EXPECT_EQ(tz.std_offset_seconds, 5 * 3600);
|
||||
EXPECT_EQ(tz.dst_offset_seconds, 4 * 3600);
|
||||
EXPECT_TRUE(tz.has_dst);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Angle-bracket notation tests (espressif/newlib-esp32#8)
|
||||
// ============================================================================
|
||||
|
||||
TEST(PosixTzParser, ParseAngleBracketPositive) {
|
||||
// Format: <+07>-7 means UTC+7 (name is "+07", offset is -7 hours east)
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz("<+07>-7", tz));
|
||||
EXPECT_EQ(tz.std_offset_seconds, -7 * 3600); // -7 = 7 hours east of UTC
|
||||
EXPECT_FALSE(tz.has_dst);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParseAngleBracketNegative) {
|
||||
// <-03>3 means UTC-3 (name is "-03", offset is 3 hours west)
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz("<-03>3", tz));
|
||||
EXPECT_EQ(tz.std_offset_seconds, 3 * 3600);
|
||||
EXPECT_FALSE(tz.has_dst);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParseAngleBracketWithDST) {
|
||||
// <+10>-10<+11>,M10.1.0,M4.1.0/3 (Australia/Sydney style)
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz("<+10>-10<+11>,M10.1.0,M4.1.0/3", tz));
|
||||
EXPECT_EQ(tz.std_offset_seconds, -10 * 3600);
|
||||
EXPECT_EQ(tz.dst_offset_seconds, -11 * 3600);
|
||||
EXPECT_TRUE(tz.has_dst);
|
||||
EXPECT_EQ(tz.dst_start.month, 10);
|
||||
EXPECT_EQ(tz.dst_end.month, 4);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParseAngleBracketNamed) {
|
||||
// <AEST>-10 (Australian Eastern Standard Time)
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz("<AEST>-10", tz));
|
||||
EXPECT_EQ(tz.std_offset_seconds, -10 * 3600);
|
||||
EXPECT_FALSE(tz.has_dst);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParseAngleBracketWithMinutes) {
|
||||
// <+0545>-5:45 (Nepal)
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz("<+0545>-5:45", tz));
|
||||
EXPECT_EQ(tz.std_offset_seconds, -(5 * 3600 + 45 * 60));
|
||||
EXPECT_FALSE(tz.has_dst);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Half-hour and unusual offset tests
|
||||
// ============================================================================
|
||||
|
||||
TEST(PosixTzParser, ParseOffsetWithMinutesIndia) {
|
||||
ParsedTimezone tz;
|
||||
// India: UTC+5:30
|
||||
ASSERT_TRUE(parse_posix_tz("IST-5:30", tz));
|
||||
EXPECT_EQ(tz.std_offset_seconds, -(5 * 3600 + 30 * 60));
|
||||
EXPECT_FALSE(tz.has_dst);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParseOffsetWithMinutesNepal) {
|
||||
ParsedTimezone tz;
|
||||
// Nepal: UTC+5:45
|
||||
ASSERT_TRUE(parse_posix_tz("NPT-5:45", tz));
|
||||
EXPECT_EQ(tz.std_offset_seconds, -(5 * 3600 + 45 * 60));
|
||||
EXPECT_FALSE(tz.has_dst);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParseOffsetWithSeconds) {
|
||||
ParsedTimezone tz;
|
||||
// Unusual but valid: offset with seconds
|
||||
ASSERT_TRUE(parse_posix_tz("TEST-1:30:30", tz));
|
||||
EXPECT_EQ(tz.std_offset_seconds, -(1 * 3600 + 30 * 60 + 30));
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParseChathamIslands) {
|
||||
// Chatham Islands: UTC+12:45 with DST
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz("<+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45", tz));
|
||||
EXPECT_EQ(tz.std_offset_seconds, -(12 * 3600 + 45 * 60));
|
||||
EXPECT_EQ(tz.dst_offset_seconds, -(13 * 3600 + 45 * 60));
|
||||
EXPECT_TRUE(tz.has_dst);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Invalid input tests
|
||||
// ============================================================================
|
||||
|
||||
TEST(PosixTzParser, ParseEmptyStringFails) {
|
||||
ParsedTimezone tz;
|
||||
EXPECT_FALSE(parse_posix_tz("", tz));
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParseNullFails) {
|
||||
ParsedTimezone tz;
|
||||
EXPECT_FALSE(parse_posix_tz(nullptr, tz));
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParseShortNameFails) {
|
||||
ParsedTimezone tz;
|
||||
// TZ name must be at least 3 characters
|
||||
EXPECT_FALSE(parse_posix_tz("AB5", tz));
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParseMissingOffsetFails) {
|
||||
ParsedTimezone tz;
|
||||
EXPECT_FALSE(parse_posix_tz("EST", tz));
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParseUnterminatedBracketFails) {
|
||||
ParsedTimezone tz;
|
||||
EXPECT_FALSE(parse_posix_tz("<+07-7", tz)); // Missing closing >
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// J-format and plain day number tests
|
||||
// ============================================================================
|
||||
|
||||
TEST(PosixTzParser, ParseJFormatBasic) {
|
||||
ParsedTimezone tz;
|
||||
// J format: Julian day 1-365, not counting Feb 29
|
||||
ASSERT_TRUE(parse_posix_tz("EST5EDT,J60,J305", tz));
|
||||
EXPECT_TRUE(tz.has_dst);
|
||||
EXPECT_EQ(tz.dst_start.type, DSTRuleType::JULIAN_NO_LEAP);
|
||||
EXPECT_EQ(tz.dst_start.day, 60); // March 1
|
||||
EXPECT_EQ(tz.dst_end.type, DSTRuleType::JULIAN_NO_LEAP);
|
||||
EXPECT_EQ(tz.dst_end.day, 305); // November 1
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParseJFormatWithTime) {
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz("EST5EDT,J60/2,J305/2", tz));
|
||||
EXPECT_EQ(tz.dst_start.day, 60);
|
||||
EXPECT_EQ(tz.dst_start.time_seconds, 2 * 3600);
|
||||
EXPECT_EQ(tz.dst_end.day, 305);
|
||||
EXPECT_EQ(tz.dst_end.time_seconds, 2 * 3600);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParsePlainDayNumber) {
|
||||
ParsedTimezone tz;
|
||||
// Plain format: day 0-365, counting Feb 29 in leap years
|
||||
ASSERT_TRUE(parse_posix_tz("EST5EDT,59,304", tz));
|
||||
EXPECT_TRUE(tz.has_dst);
|
||||
EXPECT_EQ(tz.dst_start.type, DSTRuleType::DAY_OF_YEAR);
|
||||
EXPECT_EQ(tz.dst_start.day, 59);
|
||||
EXPECT_EQ(tz.dst_end.type, DSTRuleType::DAY_OF_YEAR);
|
||||
EXPECT_EQ(tz.dst_end.day, 304);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, JFormatInvalidDayZero) {
|
||||
ParsedTimezone tz;
|
||||
// J format day must be 1-365, not 0
|
||||
EXPECT_FALSE(parse_posix_tz("EST5EDT,J0,J305", tz));
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, JFormatInvalidDay366) {
|
||||
ParsedTimezone tz;
|
||||
// J format day must be 1-365
|
||||
EXPECT_FALSE(parse_posix_tz("EST5EDT,J366,J305", tz));
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, ParsePlainDayNumberWithTime) {
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz("EST5EDT,59/3,304/1:30", tz));
|
||||
EXPECT_EQ(tz.dst_start.day, 59);
|
||||
EXPECT_EQ(tz.dst_start.time_seconds, 3 * 3600);
|
||||
EXPECT_EQ(tz.dst_end.day, 304);
|
||||
EXPECT_EQ(tz.dst_end.time_seconds, 1 * 3600 + 30 * 60);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, PlainDayInvalidDay366) {
|
||||
ParsedTimezone tz;
|
||||
// Plain format day must be 0-365
|
||||
EXPECT_FALSE(parse_posix_tz("EST5EDT,366,304", tz));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper function tests
|
||||
// ============================================================================
|
||||
|
||||
TEST(PosixTzParser, JulianDay60IsMarch1) {
|
||||
// J60 is always March 1, regardless of leap year
|
||||
int month, day;
|
||||
internal::julian_to_month_day(60, 2024, month, day); // Leap year
|
||||
EXPECT_EQ(month, 3);
|
||||
EXPECT_EQ(day, 1);
|
||||
internal::julian_to_month_day(60, 2025, month, day); // Non-leap year
|
||||
EXPECT_EQ(month, 3);
|
||||
EXPECT_EQ(day, 1);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, DayOfYear59DiffersByLeap) {
|
||||
int month, day;
|
||||
// Day 59 in leap year is Feb 29
|
||||
internal::day_of_year_to_month_day(59, 2024, month, day);
|
||||
EXPECT_EQ(month, 2);
|
||||
EXPECT_EQ(day, 29);
|
||||
// Day 59 in non-leap year is March 1
|
||||
internal::day_of_year_to_month_day(59, 2025, month, day);
|
||||
EXPECT_EQ(month, 3);
|
||||
EXPECT_EQ(day, 1);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, DayOfWeekKnownDates) {
|
||||
// January 1, 1970 was Thursday (4)
|
||||
EXPECT_EQ(internal::day_of_week(1970, 1, 1), 4);
|
||||
// January 1, 2000 was Saturday (6)
|
||||
EXPECT_EQ(internal::day_of_week(2000, 1, 1), 6);
|
||||
// March 8, 2026 is Sunday (0) - US DST start
|
||||
EXPECT_EQ(internal::day_of_week(2026, 3, 8), 0);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, LeapYearDetection) {
|
||||
EXPECT_FALSE(internal::is_leap_year(1900)); // Divisible by 100 but not 400
|
||||
EXPECT_TRUE(internal::is_leap_year(2000)); // Divisible by 400
|
||||
EXPECT_TRUE(internal::is_leap_year(2024)); // Divisible by 4
|
||||
EXPECT_FALSE(internal::is_leap_year(2025)); // Not divisible by 4
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, JulianDay1IsJan1) {
|
||||
int month, day;
|
||||
internal::julian_to_month_day(1, 2025, month, day);
|
||||
EXPECT_EQ(month, 1);
|
||||
EXPECT_EQ(day, 1);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, JulianDay365IsDec31) {
|
||||
int month, day;
|
||||
internal::julian_to_month_day(365, 2025, month, day);
|
||||
EXPECT_EQ(month, 12);
|
||||
EXPECT_EQ(day, 31);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, DayOfYear0IsJan1) {
|
||||
int month, day;
|
||||
internal::day_of_year_to_month_day(0, 2025, month, day);
|
||||
EXPECT_EQ(month, 1);
|
||||
EXPECT_EQ(day, 1);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, DaysInMonthRegular) {
|
||||
EXPECT_EQ(internal::days_in_month(2025, 1), 31);
|
||||
EXPECT_EQ(internal::days_in_month(2025, 2), 28);
|
||||
EXPECT_EQ(internal::days_in_month(2025, 4), 30);
|
||||
EXPECT_EQ(internal::days_in_month(2025, 12), 31);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, DaysInMonthLeapYear) {
|
||||
EXPECT_EQ(internal::days_in_month(2024, 2), 29);
|
||||
EXPECT_EQ(internal::days_in_month(2025, 2), 28);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DST transition calculation tests
|
||||
// ============================================================================
|
||||
|
||||
TEST(PosixTzParser, DstStartUSEastern2026) {
|
||||
// March 8, 2026 is 2nd Sunday of March
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz);
|
||||
|
||||
time_t dst_start = calculate_dst_transition(2026, tz.dst_start, tz.std_offset_seconds);
|
||||
struct tm tm;
|
||||
internal::epoch_to_tm_utc(dst_start, &tm);
|
||||
|
||||
// At 2:00 AM EST (UTC-5), so 7:00 AM UTC
|
||||
EXPECT_EQ(tm.tm_year + 1900, 2026);
|
||||
EXPECT_EQ(tm.tm_mon + 1, 3); // March
|
||||
EXPECT_EQ(tm.tm_mday, 8); // 8th
|
||||
EXPECT_EQ(tm.tm_hour, 7); // 7:00 UTC = 2:00 EST
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, DstEndUSEastern2026) {
|
||||
// November 1, 2026 is 1st Sunday of November
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz);
|
||||
|
||||
time_t dst_end = calculate_dst_transition(2026, tz.dst_end, tz.dst_offset_seconds);
|
||||
struct tm tm;
|
||||
internal::epoch_to_tm_utc(dst_end, &tm);
|
||||
|
||||
// At 2:00 AM EDT (UTC-4), so 6:00 AM UTC
|
||||
EXPECT_EQ(tm.tm_year + 1900, 2026);
|
||||
EXPECT_EQ(tm.tm_mon + 1, 11); // November
|
||||
EXPECT_EQ(tm.tm_mday, 1); // 1st
|
||||
EXPECT_EQ(tm.tm_hour, 6); // 6:00 UTC = 2:00 EDT
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, LastSundayOfMarch2026) {
|
||||
// Europe: M3.5.0 = last Sunday of March = March 29, 2026
|
||||
DSTRule rule{};
|
||||
rule.type = DSTRuleType::MONTH_WEEK_DAY;
|
||||
rule.month = 3;
|
||||
rule.week = 5;
|
||||
rule.day_of_week = 0;
|
||||
rule.time_seconds = 2 * 3600;
|
||||
time_t transition = calculate_dst_transition(2026, rule, 0);
|
||||
struct tm tm;
|
||||
internal::epoch_to_tm_utc(transition, &tm);
|
||||
EXPECT_EQ(tm.tm_mday, 29);
|
||||
EXPECT_EQ(tm.tm_wday, 0); // Sunday
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, LastSundayOfOctober2026) {
|
||||
// Europe: M10.5.0 = last Sunday of October = October 25, 2026
|
||||
DSTRule rule{};
|
||||
rule.type = DSTRuleType::MONTH_WEEK_DAY;
|
||||
rule.month = 10;
|
||||
rule.week = 5;
|
||||
rule.day_of_week = 0;
|
||||
rule.time_seconds = 3 * 3600;
|
||||
time_t transition = calculate_dst_transition(2026, rule, 0);
|
||||
struct tm tm;
|
||||
internal::epoch_to_tm_utc(transition, &tm);
|
||||
EXPECT_EQ(tm.tm_mday, 25);
|
||||
EXPECT_EQ(tm.tm_wday, 0); // Sunday
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, FirstSundayOfApril2026) {
|
||||
// April 5, 2026 is 1st Sunday
|
||||
DSTRule rule{};
|
||||
rule.type = DSTRuleType::MONTH_WEEK_DAY;
|
||||
rule.month = 4;
|
||||
rule.week = 1;
|
||||
rule.day_of_week = 0;
|
||||
rule.time_seconds = 0;
|
||||
time_t transition = calculate_dst_transition(2026, rule, 0);
|
||||
struct tm tm;
|
||||
internal::epoch_to_tm_utc(transition, &tm);
|
||||
EXPECT_EQ(tm.tm_mday, 5);
|
||||
EXPECT_EQ(tm.tm_wday, 0);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DST detection tests
|
||||
// ============================================================================
|
||||
|
||||
TEST(PosixTzParser, IsInDstUSEasternSummer) {
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz);
|
||||
|
||||
// July 4, 2026 12:00 UTC - definitely in DST
|
||||
struct tm july4 {};
|
||||
july4.tm_hour = 12;
|
||||
july4.tm_mday = 4;
|
||||
july4.tm_mon = 6;
|
||||
july4.tm_year = 126;
|
||||
time_t summer = internal::tm_to_epoch_utc(&july4);
|
||||
EXPECT_TRUE(is_in_dst(summer, tz));
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, IsInDstUSEasternWinter) {
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz);
|
||||
|
||||
// January 15, 2026 12:00 UTC - definitely not in DST
|
||||
struct tm jan15 {};
|
||||
jan15.tm_hour = 12;
|
||||
jan15.tm_mday = 15;
|
||||
jan15.tm_mon = 0;
|
||||
jan15.tm_year = 126;
|
||||
time_t winter = internal::tm_to_epoch_utc(&jan15);
|
||||
EXPECT_FALSE(is_in_dst(winter, tz));
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, IsInDstNoDstTimezone) {
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("IST-5:30", tz);
|
||||
|
||||
struct tm anytime {};
|
||||
anytime.tm_hour = 12;
|
||||
anytime.tm_mday = 15;
|
||||
anytime.tm_mon = 6;
|
||||
anytime.tm_year = 126;
|
||||
time_t epoch = internal::tm_to_epoch_utc(&anytime);
|
||||
EXPECT_FALSE(is_in_dst(epoch, tz));
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, SouthernHemisphereDstSummer) {
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("NZST-12NZDT,M9.5.0,M4.1.0/3", tz);
|
||||
|
||||
// December 15, 2025 12:00 UTC - summer in NZ, should be in DST
|
||||
struct tm dec15 {};
|
||||
dec15.tm_hour = 12;
|
||||
dec15.tm_mday = 15;
|
||||
dec15.tm_mon = 11;
|
||||
dec15.tm_year = 125;
|
||||
time_t nz_summer = internal::tm_to_epoch_utc(&dec15);
|
||||
EXPECT_TRUE(is_in_dst(nz_summer, tz));
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, SouthernHemisphereDstWinter) {
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("NZST-12NZDT,M9.5.0,M4.1.0/3", tz);
|
||||
|
||||
// July 15, 2026 12:00 UTC - winter in NZ, should NOT be in DST
|
||||
struct tm july15 {};
|
||||
july15.tm_hour = 12;
|
||||
july15.tm_mday = 15;
|
||||
july15.tm_mon = 6;
|
||||
july15.tm_year = 126;
|
||||
time_t nz_winter = internal::tm_to_epoch_utc(&july15);
|
||||
EXPECT_FALSE(is_in_dst(nz_winter, tz));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// epoch_to_local_tm tests
|
||||
// ============================================================================
|
||||
|
||||
TEST(PosixTzParser, EpochToLocalBasic) {
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("UTC0", tz);
|
||||
|
||||
time_t epoch = 0; // Jan 1, 1970 00:00:00 UTC
|
||||
struct tm local;
|
||||
ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local));
|
||||
EXPECT_EQ(local.tm_year, 70);
|
||||
EXPECT_EQ(local.tm_mon, 0);
|
||||
EXPECT_EQ(local.tm_mday, 1);
|
||||
EXPECT_EQ(local.tm_hour, 0);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, EpochToLocalWithOffset) {
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("EST5", tz); // UTC-5
|
||||
|
||||
// Jan 1, 2026 05:00:00 UTC should be Jan 1, 2026 00:00:00 EST
|
||||
struct tm utc_tm {};
|
||||
utc_tm.tm_hour = 5;
|
||||
utc_tm.tm_mday = 1;
|
||||
utc_tm.tm_mon = 0;
|
||||
utc_tm.tm_year = 126;
|
||||
time_t utc_epoch = internal::tm_to_epoch_utc(&utc_tm);
|
||||
|
||||
struct tm local;
|
||||
ASSERT_TRUE(epoch_to_local_tm(utc_epoch, tz, &local));
|
||||
EXPECT_EQ(local.tm_hour, 0); // Midnight EST
|
||||
EXPECT_EQ(local.tm_mday, 1);
|
||||
EXPECT_EQ(local.tm_isdst, 0);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, EpochToLocalDstTransition) {
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz);
|
||||
|
||||
// July 4, 2026 16:00 UTC = 12:00 EDT (noon)
|
||||
struct tm july4_utc {};
|
||||
july4_utc.tm_hour = 16;
|
||||
july4_utc.tm_mday = 4;
|
||||
july4_utc.tm_mon = 6;
|
||||
july4_utc.tm_year = 126;
|
||||
time_t utc_epoch = internal::tm_to_epoch_utc(&july4_utc);
|
||||
|
||||
struct tm local;
|
||||
ASSERT_TRUE(epoch_to_local_tm(utc_epoch, tz, &local));
|
||||
EXPECT_EQ(local.tm_hour, 12); // Noon EDT
|
||||
EXPECT_EQ(local.tm_isdst, 1);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Verification against libc
|
||||
// ============================================================================
|
||||
|
||||
class LibcVerificationTest : public ::testing::TestWithParam<std::tuple<const char *, time_t>> {};
|
||||
|
||||
TEST_P(LibcVerificationTest, MatchesLibc) {
|
||||
auto [tz_str, epoch] = GetParam();
|
||||
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz(tz_str, tz));
|
||||
|
||||
// Our implementation
|
||||
struct tm our_tm;
|
||||
epoch_to_local_tm(epoch, tz, &our_tm);
|
||||
|
||||
// libc implementation
|
||||
setenv("TZ", tz_str, 1);
|
||||
tzset();
|
||||
struct tm *libc_tm = localtime(&epoch);
|
||||
|
||||
EXPECT_EQ(our_tm.tm_year, libc_tm->tm_year);
|
||||
EXPECT_EQ(our_tm.tm_mon, libc_tm->tm_mon);
|
||||
EXPECT_EQ(our_tm.tm_mday, libc_tm->tm_mday);
|
||||
EXPECT_EQ(our_tm.tm_hour, libc_tm->tm_hour);
|
||||
EXPECT_EQ(our_tm.tm_min, libc_tm->tm_min);
|
||||
EXPECT_EQ(our_tm.tm_sec, libc_tm->tm_sec);
|
||||
EXPECT_EQ(our_tm.tm_isdst, libc_tm->tm_isdst);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(USEastern, LibcVerificationTest,
|
||||
::testing::Values(std::make_tuple("EST5EDT,M3.2.0/2,M11.1.0/2", 1704067200),
|
||||
std::make_tuple("EST5EDT,M3.2.0/2,M11.1.0/2", 1720000000),
|
||||
std::make_tuple("EST5EDT,M3.2.0/2,M11.1.0/2", 1735689600)));
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(AngleBracket, LibcVerificationTest,
|
||||
::testing::Values(std::make_tuple("<+07>-7", 1704067200),
|
||||
std::make_tuple("<+07>-7", 1720000000)));
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(India, LibcVerificationTest,
|
||||
::testing::Values(std::make_tuple("IST-5:30", 1704067200),
|
||||
std::make_tuple("IST-5:30", 1720000000)));
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(NewZealand, LibcVerificationTest,
|
||||
::testing::Values(std::make_tuple("NZST-12NZDT,M9.5.0,M4.1.0/3", 1704067200),
|
||||
std::make_tuple("NZST-12NZDT,M9.5.0,M4.1.0/3", 1720000000)));
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(USCentral, LibcVerificationTest,
|
||||
::testing::Values(std::make_tuple("CST6CDT,M3.2.0/2,M11.1.0/2", 1704067200),
|
||||
std::make_tuple("CST6CDT,M3.2.0/2,M11.1.0/2", 1720000000),
|
||||
std::make_tuple("CST6CDT,M3.2.0/2,M11.1.0/2", 1735689600)));
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(EuropeBerlin, LibcVerificationTest,
|
||||
::testing::Values(std::make_tuple("CET-1CEST,M3.5.0,M10.5.0/3", 1704067200),
|
||||
std::make_tuple("CET-1CEST,M3.5.0,M10.5.0/3", 1720000000),
|
||||
std::make_tuple("CET-1CEST,M3.5.0,M10.5.0/3", 1735689600)));
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(AustraliaSydney, LibcVerificationTest,
|
||||
::testing::Values(std::make_tuple("AEST-10AEDT,M10.1.0,M4.1.0/3", 1704067200),
|
||||
std::make_tuple("AEST-10AEDT,M10.1.0,M4.1.0/3", 1720000000),
|
||||
std::make_tuple("AEST-10AEDT,M10.1.0,M4.1.0/3", 1735689600)));
|
||||
|
||||
// ============================================================================
|
||||
// DST boundary edge cases
|
||||
// ============================================================================
|
||||
|
||||
TEST(PosixTzParser, DstBoundaryJustBeforeSpringForward) {
|
||||
// Test 1 second before DST starts
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz);
|
||||
|
||||
// March 8, 2026 06:59:59 UTC = 01:59:59 EST (1 second before spring forward)
|
||||
struct tm before {};
|
||||
before.tm_sec = 59;
|
||||
before.tm_min = 59;
|
||||
before.tm_hour = 6;
|
||||
before.tm_mday = 8;
|
||||
before.tm_mon = 2;
|
||||
before.tm_year = 126;
|
||||
time_t before_epoch = internal::tm_to_epoch_utc(&before);
|
||||
EXPECT_FALSE(is_in_dst(before_epoch, tz));
|
||||
|
||||
// March 8, 2026 07:00:00 UTC = 02:00:00 EST -> 03:00:00 EDT (DST started)
|
||||
struct tm after {};
|
||||
after.tm_hour = 7;
|
||||
after.tm_mday = 8;
|
||||
after.tm_mon = 2;
|
||||
after.tm_year = 126;
|
||||
time_t after_epoch = internal::tm_to_epoch_utc(&after);
|
||||
EXPECT_TRUE(is_in_dst(after_epoch, tz));
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, DstBoundaryJustBeforeFallBack) {
|
||||
// Test 1 second before DST ends
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz);
|
||||
|
||||
// November 1, 2026 05:59:59 UTC = 01:59:59 EDT (1 second before fall back)
|
||||
struct tm before {};
|
||||
before.tm_sec = 59;
|
||||
before.tm_min = 59;
|
||||
before.tm_hour = 5;
|
||||
before.tm_mday = 1;
|
||||
before.tm_mon = 10;
|
||||
before.tm_year = 126;
|
||||
time_t before_epoch = internal::tm_to_epoch_utc(&before);
|
||||
EXPECT_TRUE(is_in_dst(before_epoch, tz));
|
||||
|
||||
// November 1, 2026 06:00:00 UTC = 02:00:00 EDT -> 01:00:00 EST (DST ended)
|
||||
struct tm after {};
|
||||
after.tm_hour = 6;
|
||||
after.tm_mday = 1;
|
||||
after.tm_mon = 10;
|
||||
after.tm_year = 126;
|
||||
time_t after_epoch = internal::tm_to_epoch_utc(&after);
|
||||
EXPECT_FALSE(is_in_dst(after_epoch, tz));
|
||||
}
|
||||
|
||||
} // namespace esphome::time::testing
|
||||
@@ -1,884 +0,0 @@
|
||||
// Test POSIX TZ parser implementation
|
||||
// Compile with: g++ -std=gnu++20 -I../../esphome/core -o test_posix_tz_parser test_posix_tz_parser.cpp
|
||||
// ../../esphome/core/posix_tz.cpp && ./test_posix_tz_parser
|
||||
//
|
||||
// This test verifies our custom POSIX TZ parser produces identical results to libc's
|
||||
// tzset()/localtime() implementation. The custom parser avoids pulling in scanf (~7.6KB).
|
||||
//
|
||||
// Key test cases include:
|
||||
// - Angle-bracket timezone notation (<+07>-7) - see espressif/newlib-esp32#8
|
||||
// - Half-hour offsets (IST-5:30)
|
||||
// - Southern hemisphere DST (start month > end month)
|
||||
// - DST transition boundary conditions
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
|
||||
// Include the implementation directly for standalone compilation
|
||||
#include "../../esphome/core/posix_tz.h"
|
||||
#include "../../esphome/core/posix_tz.cpp"
|
||||
|
||||
using namespace esphome;
|
||||
|
||||
#define TEST(name) static void test_##name()
|
||||
#define RUN_TEST(name) \
|
||||
do { \
|
||||
printf(" " #name "..."); \
|
||||
fflush(stdout); \
|
||||
test_##name(); \
|
||||
printf(" OK\n"); \
|
||||
} while (0)
|
||||
|
||||
// ============================================================================
|
||||
// Basic TZ string parsing tests
|
||||
// ============================================================================
|
||||
|
||||
TEST(parse_simple_offset_est5) {
|
||||
ParsedTimezone tz;
|
||||
assert(parse_posix_tz("EST5", tz));
|
||||
assert(tz.std_offset_seconds == 5 * 3600); // +5 hours (west of UTC)
|
||||
assert(!tz.has_dst);
|
||||
}
|
||||
|
||||
TEST(parse_negative_offset_cet) {
|
||||
ParsedTimezone tz;
|
||||
assert(parse_posix_tz("CET-1", tz));
|
||||
assert(tz.std_offset_seconds == -1 * 3600); // -1 hour (east of UTC)
|
||||
assert(!tz.has_dst);
|
||||
}
|
||||
|
||||
TEST(parse_explicit_positive_offset) {
|
||||
ParsedTimezone tz;
|
||||
assert(parse_posix_tz("TEST+5", tz));
|
||||
assert(tz.std_offset_seconds == 5 * 3600);
|
||||
assert(!tz.has_dst);
|
||||
}
|
||||
|
||||
TEST(parse_zero_offset) {
|
||||
ParsedTimezone tz;
|
||||
assert(parse_posix_tz("UTC0", tz));
|
||||
assert(tz.std_offset_seconds == 0);
|
||||
assert(!tz.has_dst);
|
||||
}
|
||||
|
||||
TEST(parse_us_eastern_with_dst) {
|
||||
ParsedTimezone tz;
|
||||
assert(parse_posix_tz("EST5EDT,M3.2.0,M11.1.0", tz));
|
||||
assert(tz.std_offset_seconds == 5 * 3600);
|
||||
assert(tz.dst_offset_seconds == 4 * 3600); // Default: STD - 1hr
|
||||
assert(tz.has_dst);
|
||||
assert(tz.dst_start.month == 3);
|
||||
assert(tz.dst_start.week == 2);
|
||||
assert(tz.dst_start.day_of_week == 0); // Sunday
|
||||
assert(tz.dst_start.time_seconds == 2 * 3600); // Default 2:00 AM
|
||||
assert(tz.dst_end.month == 11);
|
||||
assert(tz.dst_end.week == 1);
|
||||
assert(tz.dst_end.day_of_week == 0);
|
||||
}
|
||||
|
||||
TEST(parse_us_central_with_time) {
|
||||
ParsedTimezone tz;
|
||||
assert(parse_posix_tz("CST6CDT,M3.2.0/2,M11.1.0/2", tz));
|
||||
assert(tz.std_offset_seconds == 6 * 3600);
|
||||
assert(tz.dst_offset_seconds == 5 * 3600);
|
||||
assert(tz.dst_start.time_seconds == 2 * 3600); // 2:00 AM
|
||||
assert(tz.dst_end.time_seconds == 2 * 3600);
|
||||
}
|
||||
|
||||
TEST(parse_europe_berlin) {
|
||||
ParsedTimezone tz;
|
||||
assert(parse_posix_tz("CET-1CEST,M3.5.0,M10.5.0/3", tz));
|
||||
assert(tz.std_offset_seconds == -1 * 3600);
|
||||
assert(tz.dst_offset_seconds == -2 * 3600); // Default: STD - 1hr
|
||||
assert(tz.has_dst);
|
||||
assert(tz.dst_start.month == 3);
|
||||
assert(tz.dst_start.week == 5); // Last week
|
||||
assert(tz.dst_end.month == 10);
|
||||
assert(tz.dst_end.week == 5); // Last week
|
||||
assert(tz.dst_end.time_seconds == 3 * 3600); // 3:00 AM
|
||||
}
|
||||
|
||||
TEST(parse_new_zealand) {
|
||||
ParsedTimezone tz;
|
||||
// Southern hemisphere - DST starts in Sept, ends in April
|
||||
assert(parse_posix_tz("NZST-12NZDT,M9.5.0,M4.1.0/3", tz));
|
||||
assert(tz.std_offset_seconds == -12 * 3600);
|
||||
assert(tz.dst_offset_seconds == -13 * 3600); // Default: STD - 1hr
|
||||
assert(tz.has_dst);
|
||||
assert(tz.dst_start.month == 9); // September
|
||||
assert(tz.dst_end.month == 4); // April
|
||||
}
|
||||
|
||||
TEST(parse_explicit_dst_offset) {
|
||||
ParsedTimezone tz;
|
||||
// Some places have non-standard DST offsets
|
||||
assert(parse_posix_tz("TEST5DST4,M3.2.0,M11.1.0", tz));
|
||||
assert(tz.std_offset_seconds == 5 * 3600);
|
||||
assert(tz.dst_offset_seconds == 4 * 3600);
|
||||
assert(tz.has_dst);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Angle-bracket notation tests (espressif/newlib-esp32#8)
|
||||
// ============================================================================
|
||||
|
||||
TEST(parse_angle_bracket_positive) {
|
||||
// Format: <+07>-7 means UTC+7 (name is "+07", offset is -7 hours east)
|
||||
ParsedTimezone tz;
|
||||
assert(parse_posix_tz("<+07>-7", tz));
|
||||
assert(tz.std_offset_seconds == -7 * 3600); // -7 = 7 hours east of UTC
|
||||
assert(!tz.has_dst);
|
||||
}
|
||||
|
||||
TEST(parse_angle_bracket_negative) {
|
||||
// <-03>3 means UTC-3 (name is "-03", offset is 3 hours west)
|
||||
ParsedTimezone tz;
|
||||
assert(parse_posix_tz("<-03>3", tz));
|
||||
assert(tz.std_offset_seconds == 3 * 3600);
|
||||
assert(!tz.has_dst);
|
||||
}
|
||||
|
||||
TEST(parse_angle_bracket_with_dst) {
|
||||
// <+10>-10<+11>,M10.1.0,M4.1.0/3 (Australia/Sydney style)
|
||||
ParsedTimezone tz;
|
||||
assert(parse_posix_tz("<+10>-10<+11>,M10.1.0,M4.1.0/3", tz));
|
||||
assert(tz.std_offset_seconds == -10 * 3600);
|
||||
assert(tz.dst_offset_seconds == -11 * 3600);
|
||||
assert(tz.has_dst);
|
||||
assert(tz.dst_start.month == 10);
|
||||
assert(tz.dst_end.month == 4);
|
||||
}
|
||||
|
||||
TEST(parse_angle_bracket_named) {
|
||||
// <AEST>-10 (Australian Eastern Standard Time)
|
||||
ParsedTimezone tz;
|
||||
assert(parse_posix_tz("<AEST>-10", tz));
|
||||
assert(tz.std_offset_seconds == -10 * 3600);
|
||||
assert(!tz.has_dst);
|
||||
}
|
||||
|
||||
TEST(parse_angle_bracket_with_minutes) {
|
||||
// <+0545>-5:45 (Nepal)
|
||||
ParsedTimezone tz;
|
||||
assert(parse_posix_tz("<+0545>-5:45", tz));
|
||||
assert(tz.std_offset_seconds == -(5 * 3600 + 45 * 60));
|
||||
assert(!tz.has_dst);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Half-hour and unusual offset tests
|
||||
// ============================================================================
|
||||
|
||||
TEST(parse_offset_with_minutes_india) {
|
||||
ParsedTimezone tz;
|
||||
// India: UTC+5:30
|
||||
assert(parse_posix_tz("IST-5:30", tz));
|
||||
assert(tz.std_offset_seconds == -(5 * 3600 + 30 * 60));
|
||||
assert(!tz.has_dst);
|
||||
}
|
||||
|
||||
TEST(parse_offset_with_minutes_nepal) {
|
||||
ParsedTimezone tz;
|
||||
// Nepal: UTC+5:45
|
||||
assert(parse_posix_tz("NPT-5:45", tz));
|
||||
assert(tz.std_offset_seconds == -(5 * 3600 + 45 * 60));
|
||||
assert(!tz.has_dst);
|
||||
}
|
||||
|
||||
TEST(parse_offset_with_seconds) {
|
||||
ParsedTimezone tz;
|
||||
// Unusual but valid: offset with seconds
|
||||
assert(parse_posix_tz("TEST-1:30:30", tz));
|
||||
assert(tz.std_offset_seconds == -(1 * 3600 + 30 * 60 + 30));
|
||||
}
|
||||
|
||||
TEST(parse_chatham_islands) {
|
||||
// Chatham Islands: UTC+12:45 with DST
|
||||
ParsedTimezone tz;
|
||||
assert(parse_posix_tz("<+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45", tz));
|
||||
assert(tz.std_offset_seconds == -(12 * 3600 + 45 * 60));
|
||||
assert(tz.dst_offset_seconds == -(13 * 3600 + 45 * 60));
|
||||
assert(tz.has_dst);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Invalid input tests
|
||||
// ============================================================================
|
||||
|
||||
TEST(parse_empty_string_fails) {
|
||||
ParsedTimezone tz;
|
||||
assert(!parse_posix_tz("", tz));
|
||||
}
|
||||
|
||||
TEST(parse_null_fails) {
|
||||
ParsedTimezone tz;
|
||||
assert(!parse_posix_tz(nullptr, tz));
|
||||
}
|
||||
|
||||
TEST(parse_short_name_fails) {
|
||||
ParsedTimezone tz;
|
||||
// TZ name must be at least 3 characters
|
||||
assert(!parse_posix_tz("AB5", tz));
|
||||
}
|
||||
|
||||
TEST(parse_missing_offset_fails) {
|
||||
ParsedTimezone tz;
|
||||
assert(!parse_posix_tz("EST", tz));
|
||||
}
|
||||
|
||||
TEST(parse_unterminated_bracket_fails) {
|
||||
ParsedTimezone tz;
|
||||
assert(!parse_posix_tz("<+07-7", tz)); // Missing closing >
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// J-format and plain day number tests
|
||||
// ============================================================================
|
||||
|
||||
TEST(parse_j_format_basic) {
|
||||
ParsedTimezone tz;
|
||||
// J format: Julian day 1-365, not counting Feb 29
|
||||
assert(parse_posix_tz("EST5EDT,J60,J305", tz));
|
||||
assert(tz.has_dst);
|
||||
assert(tz.dst_start.type == DSTRuleType::JULIAN_NO_LEAP);
|
||||
assert(tz.dst_start.day == 60); // March 1
|
||||
assert(tz.dst_end.type == DSTRuleType::JULIAN_NO_LEAP);
|
||||
assert(tz.dst_end.day == 305); // November 1
|
||||
}
|
||||
|
||||
TEST(parse_j_format_with_time) {
|
||||
ParsedTimezone tz;
|
||||
assert(parse_posix_tz("EST5EDT,J60/2,J305/2", tz));
|
||||
assert(tz.dst_start.day == 60);
|
||||
assert(tz.dst_start.time_seconds == 2 * 3600);
|
||||
assert(tz.dst_end.day == 305);
|
||||
assert(tz.dst_end.time_seconds == 2 * 3600);
|
||||
}
|
||||
|
||||
TEST(parse_plain_day_number) {
|
||||
ParsedTimezone tz;
|
||||
// Plain format: day 0-365, counting Feb 29 in leap years
|
||||
assert(parse_posix_tz("EST5EDT,59,304", tz));
|
||||
assert(tz.has_dst);
|
||||
assert(tz.dst_start.type == DSTRuleType::DAY_OF_YEAR);
|
||||
assert(tz.dst_start.day == 59); // Feb 29 or March 1 depending on leap year
|
||||
assert(tz.dst_end.type == DSTRuleType::DAY_OF_YEAR);
|
||||
assert(tz.dst_end.day == 304);
|
||||
}
|
||||
|
||||
TEST(parse_plain_day_number_with_time) {
|
||||
ParsedTimezone tz;
|
||||
assert(parse_posix_tz("EST5EDT,59/3,304/1:30", tz));
|
||||
assert(tz.dst_start.day == 59);
|
||||
assert(tz.dst_start.time_seconds == 3 * 3600);
|
||||
assert(tz.dst_end.day == 304);
|
||||
assert(tz.dst_end.time_seconds == 1 * 3600 + 30 * 60);
|
||||
}
|
||||
|
||||
TEST(j_format_invalid_day_zero) {
|
||||
ParsedTimezone tz;
|
||||
// J format day must be 1-365, not 0
|
||||
assert(!parse_posix_tz("EST5EDT,J0,J305", tz));
|
||||
}
|
||||
|
||||
TEST(j_format_invalid_day_366) {
|
||||
ParsedTimezone tz;
|
||||
// J format day must be 1-365
|
||||
assert(!parse_posix_tz("EST5EDT,J366,J305", tz));
|
||||
}
|
||||
|
||||
TEST(plain_day_invalid_day_366) {
|
||||
ParsedTimezone tz;
|
||||
// Plain format day must be 0-365
|
||||
assert(!parse_posix_tz("EST5EDT,366,304", tz));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Julian day to month/day conversion tests
|
||||
// ============================================================================
|
||||
|
||||
TEST(julian_day_60_is_march_1) {
|
||||
// J60 is always March 1, regardless of leap year
|
||||
int month, day;
|
||||
internal::julian_to_month_day(60, 2024, month, day); // Leap year
|
||||
assert(month == 3 && day == 1);
|
||||
internal::julian_to_month_day(60, 2025, month, day); // Non-leap year
|
||||
assert(month == 3 && day == 1);
|
||||
}
|
||||
|
||||
TEST(julian_day_1_is_jan_1) {
|
||||
int month, day;
|
||||
internal::julian_to_month_day(1, 2025, month, day);
|
||||
assert(month == 1 && day == 1);
|
||||
}
|
||||
|
||||
TEST(julian_day_365_is_dec_31) {
|
||||
int month, day;
|
||||
internal::julian_to_month_day(365, 2025, month, day);
|
||||
assert(month == 12 && day == 31);
|
||||
}
|
||||
|
||||
TEST(day_of_year_59_differs_by_leap) {
|
||||
int month, day;
|
||||
// Day 59 in leap year is Feb 29
|
||||
internal::day_of_year_to_month_day(59, 2024, month, day);
|
||||
assert(month == 2 && day == 29);
|
||||
// Day 59 in non-leap year is March 1
|
||||
internal::day_of_year_to_month_day(59, 2025, month, day);
|
||||
assert(month == 3 && day == 1);
|
||||
}
|
||||
|
||||
TEST(day_of_year_0_is_jan_1) {
|
||||
int month, day;
|
||||
internal::day_of_year_to_month_day(0, 2025, month, day);
|
||||
assert(month == 1 && day == 1);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Day of week calculation tests
|
||||
// ============================================================================
|
||||
|
||||
TEST(day_of_week_known_dates) {
|
||||
// January 1, 1970 was Thursday (4)
|
||||
assert(internal::day_of_week(1970, 1, 1) == 4);
|
||||
|
||||
// July 4, 1776 was Thursday (4)
|
||||
assert(internal::day_of_week(1776, 7, 4) == 4);
|
||||
|
||||
// January 1, 2000 was Saturday (6)
|
||||
assert(internal::day_of_week(2000, 1, 1) == 6);
|
||||
|
||||
// September 11, 2001 was Tuesday (2)
|
||||
assert(internal::day_of_week(2001, 9, 11) == 2);
|
||||
|
||||
// March 8, 2026 is Sunday (0) - US DST start
|
||||
assert(internal::day_of_week(2026, 3, 8) == 0);
|
||||
|
||||
// November 1, 2026 is Sunday (0) - US DST end
|
||||
assert(internal::day_of_week(2026, 11, 1) == 0);
|
||||
}
|
||||
|
||||
TEST(leap_year_detection) {
|
||||
assert(!internal::is_leap_year(1900)); // Divisible by 100 but not 400
|
||||
assert(internal::is_leap_year(2000)); // Divisible by 400
|
||||
assert(internal::is_leap_year(2024)); // Divisible by 4
|
||||
assert(!internal::is_leap_year(2025)); // Not divisible by 4
|
||||
assert(internal::is_leap_year(2028));
|
||||
}
|
||||
|
||||
TEST(days_in_month_regular) {
|
||||
assert(internal::days_in_month(2025, 1) == 31);
|
||||
assert(internal::days_in_month(2025, 2) == 28);
|
||||
assert(internal::days_in_month(2025, 4) == 30);
|
||||
assert(internal::days_in_month(2025, 12) == 31);
|
||||
}
|
||||
|
||||
TEST(days_in_month_leap_year) {
|
||||
assert(internal::days_in_month(2024, 2) == 29);
|
||||
assert(internal::days_in_month(2025, 2) == 28);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DST transition calculation tests
|
||||
// ============================================================================
|
||||
|
||||
TEST(dst_start_us_eastern_2026) {
|
||||
// March 8, 2026 is 2nd Sunday of March
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz);
|
||||
|
||||
time_t dst_start = calculate_dst_transition(2026, tz.dst_start, tz.std_offset_seconds);
|
||||
struct tm tm;
|
||||
internal::epoch_to_tm_utc(dst_start, &tm);
|
||||
|
||||
// At 2:00 AM EST (UTC-5), so 7:00 AM UTC
|
||||
assert(tm.tm_year + 1900 == 2026);
|
||||
assert(tm.tm_mon + 1 == 3); // March
|
||||
assert(tm.tm_mday == 8); // 8th
|
||||
assert(tm.tm_hour == 7); // 7:00 UTC = 2:00 EST
|
||||
}
|
||||
|
||||
TEST(dst_end_us_eastern_2026) {
|
||||
// November 1, 2026 is 1st Sunday of November
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz);
|
||||
|
||||
time_t dst_end = calculate_dst_transition(2026, tz.dst_end, tz.dst_offset_seconds);
|
||||
struct tm tm;
|
||||
internal::epoch_to_tm_utc(dst_end, &tm);
|
||||
|
||||
// At 2:00 AM EDT (UTC-4), so 6:00 AM UTC
|
||||
assert(tm.tm_year + 1900 == 2026);
|
||||
assert(tm.tm_mon + 1 == 11); // November
|
||||
assert(tm.tm_mday == 1); // 1st
|
||||
assert(tm.tm_hour == 6); // 6:00 UTC = 2:00 EDT
|
||||
}
|
||||
|
||||
TEST(last_sunday_of_march_2026) {
|
||||
// Europe: M3.5.0 = last Sunday of March = March 29, 2026
|
||||
DSTRule rule{};
|
||||
rule.type = DSTRuleType::MONTH_WEEK_DAY;
|
||||
rule.month = 3;
|
||||
rule.week = 5;
|
||||
rule.day_of_week = 0;
|
||||
rule.time_seconds = 2 * 3600;
|
||||
time_t transition = calculate_dst_transition(2026, rule, 0);
|
||||
struct tm tm;
|
||||
internal::epoch_to_tm_utc(transition, &tm);
|
||||
assert(tm.tm_mday == 29);
|
||||
assert(tm.tm_wday == 0); // Sunday
|
||||
}
|
||||
|
||||
TEST(last_sunday_of_october_2026) {
|
||||
// Europe: M10.5.0 = last Sunday of October = October 25, 2026
|
||||
DSTRule rule{};
|
||||
rule.type = DSTRuleType::MONTH_WEEK_DAY;
|
||||
rule.month = 10;
|
||||
rule.week = 5;
|
||||
rule.day_of_week = 0;
|
||||
rule.time_seconds = 3 * 3600;
|
||||
time_t transition = calculate_dst_transition(2026, rule, 0);
|
||||
struct tm tm;
|
||||
internal::epoch_to_tm_utc(transition, &tm);
|
||||
assert(tm.tm_mday == 25);
|
||||
assert(tm.tm_wday == 0); // Sunday
|
||||
}
|
||||
|
||||
TEST(first_sunday_of_april_2026) {
|
||||
// April 5, 2026 is 1st Sunday
|
||||
DSTRule rule{};
|
||||
rule.type = DSTRuleType::MONTH_WEEK_DAY;
|
||||
rule.month = 4;
|
||||
rule.week = 1;
|
||||
rule.day_of_week = 0;
|
||||
rule.time_seconds = 0;
|
||||
time_t transition = calculate_dst_transition(2026, rule, 0);
|
||||
struct tm tm;
|
||||
internal::epoch_to_tm_utc(transition, &tm);
|
||||
assert(tm.tm_mday == 5);
|
||||
assert(tm.tm_wday == 0);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// is_in_dst tests
|
||||
// ============================================================================
|
||||
|
||||
TEST(is_in_dst_us_eastern_summer) {
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz);
|
||||
|
||||
// July 4, 2026 12:00 UTC - definitely in DST
|
||||
struct tm july4 {};
|
||||
july4.tm_sec = 0;
|
||||
july4.tm_min = 0;
|
||||
july4.tm_hour = 12;
|
||||
july4.tm_mday = 4;
|
||||
july4.tm_mon = 6;
|
||||
july4.tm_year = 126;
|
||||
time_t summer = internal::tm_to_epoch_utc(&july4);
|
||||
assert(is_in_dst(summer, tz) == true);
|
||||
}
|
||||
|
||||
TEST(is_in_dst_us_eastern_winter) {
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz);
|
||||
|
||||
// January 15, 2026 12:00 UTC - definitely not in DST
|
||||
struct tm jan15 {};
|
||||
jan15.tm_sec = 0;
|
||||
jan15.tm_min = 0;
|
||||
jan15.tm_hour = 12;
|
||||
jan15.tm_mday = 15;
|
||||
jan15.tm_mon = 0;
|
||||
jan15.tm_year = 126;
|
||||
time_t winter = internal::tm_to_epoch_utc(&jan15);
|
||||
assert(is_in_dst(winter, tz) == false);
|
||||
}
|
||||
|
||||
TEST(is_in_dst_no_dst_timezone) {
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("IST-5:30", tz);
|
||||
|
||||
struct tm anytime {};
|
||||
anytime.tm_sec = 0;
|
||||
anytime.tm_min = 0;
|
||||
anytime.tm_hour = 12;
|
||||
anytime.tm_mday = 15;
|
||||
anytime.tm_mon = 6;
|
||||
anytime.tm_year = 126;
|
||||
time_t epoch = internal::tm_to_epoch_utc(&anytime);
|
||||
assert(is_in_dst(epoch, tz) == false);
|
||||
}
|
||||
|
||||
TEST(southern_hemisphere_dst_summer) {
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("NZST-12NZDT,M9.5.0,M4.1.0/3", tz);
|
||||
|
||||
// December 15, 2025 12:00 UTC - summer in NZ, should be in DST
|
||||
struct tm dec15 {};
|
||||
dec15.tm_sec = 0;
|
||||
dec15.tm_min = 0;
|
||||
dec15.tm_hour = 12;
|
||||
dec15.tm_mday = 15;
|
||||
dec15.tm_mon = 11;
|
||||
dec15.tm_year = 125;
|
||||
time_t nz_summer = internal::tm_to_epoch_utc(&dec15);
|
||||
assert(is_in_dst(nz_summer, tz) == true);
|
||||
}
|
||||
|
||||
TEST(southern_hemisphere_dst_winter) {
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("NZST-12NZDT,M9.5.0,M4.1.0/3", tz);
|
||||
|
||||
// July 15, 2026 12:00 UTC - winter in NZ, should NOT be in DST
|
||||
struct tm july15 {};
|
||||
july15.tm_sec = 0;
|
||||
july15.tm_min = 0;
|
||||
july15.tm_hour = 12;
|
||||
july15.tm_mday = 15;
|
||||
july15.tm_mon = 6;
|
||||
july15.tm_year = 126;
|
||||
time_t nz_winter = internal::tm_to_epoch_utc(&july15);
|
||||
assert(is_in_dst(nz_winter, tz) == false);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// epoch_to_local_tm tests
|
||||
// ============================================================================
|
||||
|
||||
TEST(epoch_to_local_basic) {
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("UTC0", tz);
|
||||
|
||||
time_t epoch = 0; // Jan 1, 1970 00:00:00 UTC
|
||||
struct tm local;
|
||||
assert(epoch_to_local_tm(epoch, tz, &local));
|
||||
assert(local.tm_year == 70);
|
||||
assert(local.tm_mon == 0);
|
||||
assert(local.tm_mday == 1);
|
||||
assert(local.tm_hour == 0);
|
||||
}
|
||||
|
||||
TEST(epoch_to_local_with_offset) {
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("EST5", tz); // UTC-5
|
||||
|
||||
// Jan 1, 2026 05:00:00 UTC should be Jan 1, 2026 00:00:00 EST
|
||||
struct tm utc_tm {};
|
||||
utc_tm.tm_sec = 0;
|
||||
utc_tm.tm_min = 0;
|
||||
utc_tm.tm_hour = 5;
|
||||
utc_tm.tm_mday = 1;
|
||||
utc_tm.tm_mon = 0;
|
||||
utc_tm.tm_year = 126;
|
||||
time_t utc_epoch = internal::tm_to_epoch_utc(&utc_tm);
|
||||
|
||||
struct tm local;
|
||||
assert(epoch_to_local_tm(utc_epoch, tz, &local));
|
||||
assert(local.tm_hour == 0); // Midnight EST
|
||||
assert(local.tm_mday == 1);
|
||||
assert(local.tm_isdst == 0);
|
||||
}
|
||||
|
||||
TEST(epoch_to_local_dst_transition) {
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz);
|
||||
|
||||
// July 4, 2026 16:00 UTC = 12:00 EDT (noon)
|
||||
struct tm july4_utc {};
|
||||
july4_utc.tm_sec = 0;
|
||||
july4_utc.tm_min = 0;
|
||||
july4_utc.tm_hour = 16;
|
||||
july4_utc.tm_mday = 4;
|
||||
july4_utc.tm_mon = 6;
|
||||
july4_utc.tm_year = 126;
|
||||
time_t utc_epoch = internal::tm_to_epoch_utc(&july4_utc);
|
||||
|
||||
struct tm local;
|
||||
assert(epoch_to_local_tm(utc_epoch, tz, &local));
|
||||
assert(local.tm_hour == 12); // Noon EDT
|
||||
assert(local.tm_isdst == 1);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Verification against libc (run on desktop only)
|
||||
// ============================================================================
|
||||
|
||||
// Helper to compare our implementation against libc
|
||||
static bool verify_against_libc(const char *tz_str, time_t epoch) {
|
||||
ParsedTimezone tz;
|
||||
if (!parse_posix_tz(tz_str, tz)) {
|
||||
printf("Failed to parse TZ: %s\n", tz_str);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Our implementation
|
||||
struct tm our_tm;
|
||||
epoch_to_local_tm(epoch, tz, &our_tm);
|
||||
|
||||
// libc implementation
|
||||
setenv("TZ", tz_str, 1);
|
||||
tzset();
|
||||
struct tm *libc_tm = localtime(&epoch);
|
||||
|
||||
bool match =
|
||||
(our_tm.tm_year == libc_tm->tm_year && our_tm.tm_mon == libc_tm->tm_mon && our_tm.tm_mday == libc_tm->tm_mday &&
|
||||
our_tm.tm_hour == libc_tm->tm_hour && our_tm.tm_min == libc_tm->tm_min && our_tm.tm_sec == libc_tm->tm_sec &&
|
||||
our_tm.tm_isdst == libc_tm->tm_isdst);
|
||||
|
||||
if (!match) {
|
||||
printf("\nMismatch for TZ=%s epoch=%ld\n", tz_str, (long) epoch);
|
||||
printf(" Our: %04d-%02d-%02d %02d:%02d:%02d DST=%d\n", our_tm.tm_year + 1900, our_tm.tm_mon + 1, our_tm.tm_mday,
|
||||
our_tm.tm_hour, our_tm.tm_min, our_tm.tm_sec, our_tm.tm_isdst);
|
||||
printf(" libc: %04d-%02d-%02d %02d:%02d:%02d DST=%d\n", libc_tm->tm_year + 1900, libc_tm->tm_mon + 1,
|
||||
libc_tm->tm_mday, libc_tm->tm_hour, libc_tm->tm_min, libc_tm->tm_sec, libc_tm->tm_isdst);
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
TEST(verify_us_eastern_multiple_epochs) {
|
||||
const char *tz_str = "EST5EDT,M3.2.0/2,M11.1.0/2";
|
||||
// Test various dates throughout the year
|
||||
time_t epochs[] = {
|
||||
1704067200, // Jan 1, 2024 00:00 UTC
|
||||
1711900800, // March 31, 2024 12:00 UTC (after DST start)
|
||||
1720000000, // July 3, 2024 (summer)
|
||||
1730419200, // Nov 1, 2024 00:00 UTC (DST end day)
|
||||
1735689600, // Jan 1, 2025 00:00 UTC
|
||||
};
|
||||
for (time_t epoch : epochs) {
|
||||
assert(verify_against_libc(tz_str, epoch));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(verify_us_central_multiple_epochs) {
|
||||
const char *tz_str = "CST6CDT,M3.2.0/2,M11.1.0/2";
|
||||
time_t epochs[] = {
|
||||
1704067200, 1711900800, 1720000000, 1730419200, 1735689600,
|
||||
};
|
||||
for (time_t epoch : epochs) {
|
||||
assert(verify_against_libc(tz_str, epoch));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(verify_europe_berlin_multiple_epochs) {
|
||||
const char *tz_str = "CET-1CEST,M3.5.0,M10.5.0/3";
|
||||
time_t epochs[] = {
|
||||
1704067200, 1711900800, 1720000000, 1730419200, 1735689600,
|
||||
};
|
||||
for (time_t epoch : epochs) {
|
||||
assert(verify_against_libc(tz_str, epoch));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(verify_angle_bracket_notation) {
|
||||
// This was the bug in espressif/newlib-esp32#8
|
||||
const char *tz_str = "<+07>-7";
|
||||
time_t epochs[] = {
|
||||
1704067200,
|
||||
1720000000,
|
||||
1735689600,
|
||||
};
|
||||
for (time_t epoch : epochs) {
|
||||
assert(verify_against_libc(tz_str, epoch));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(verify_india_half_hour) {
|
||||
const char *tz_str = "IST-5:30";
|
||||
time_t epochs[] = {
|
||||
1704067200,
|
||||
1720000000,
|
||||
1735689600,
|
||||
};
|
||||
for (time_t epoch : epochs) {
|
||||
assert(verify_against_libc(tz_str, epoch));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(verify_new_zealand_southern_hemisphere) {
|
||||
const char *tz_str = "NZST-12NZDT,M9.5.0,M4.1.0/3";
|
||||
time_t epochs[] = {
|
||||
1704067200, // Jan (NZ summer, DST)
|
||||
1720000000, // July (NZ winter, no DST)
|
||||
1735689600, // Jan (NZ summer, DST)
|
||||
};
|
||||
for (time_t epoch : epochs) {
|
||||
assert(verify_against_libc(tz_str, epoch));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(verify_australia_sydney) {
|
||||
const char *tz_str = "AEST-10AEDT,M10.1.0,M4.1.0/3";
|
||||
time_t epochs[] = {
|
||||
1704067200,
|
||||
1720000000,
|
||||
1735689600,
|
||||
};
|
||||
for (time_t epoch : epochs) {
|
||||
assert(verify_against_libc(tz_str, epoch));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DST boundary edge cases
|
||||
// ============================================================================
|
||||
|
||||
TEST(dst_boundary_just_before_spring_forward) {
|
||||
// Test 1 second before DST starts
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz);
|
||||
|
||||
// March 8, 2026 06:59:59 UTC = 01:59:59 EST (1 second before spring forward)
|
||||
struct tm before {};
|
||||
before.tm_sec = 59;
|
||||
before.tm_min = 59;
|
||||
before.tm_hour = 6;
|
||||
before.tm_mday = 8;
|
||||
before.tm_mon = 2;
|
||||
before.tm_year = 126;
|
||||
time_t before_epoch = internal::tm_to_epoch_utc(&before);
|
||||
assert(is_in_dst(before_epoch, tz) == false);
|
||||
|
||||
// March 8, 2026 07:00:00 UTC = 02:00:00 EST -> 03:00:00 EDT (DST started)
|
||||
struct tm after {};
|
||||
after.tm_sec = 0;
|
||||
after.tm_min = 0;
|
||||
after.tm_hour = 7;
|
||||
after.tm_mday = 8;
|
||||
after.tm_mon = 2;
|
||||
after.tm_year = 126;
|
||||
time_t after_epoch = internal::tm_to_epoch_utc(&after);
|
||||
assert(is_in_dst(after_epoch, tz) == true);
|
||||
}
|
||||
|
||||
TEST(dst_boundary_just_before_fall_back) {
|
||||
// Test 1 second before DST ends
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz);
|
||||
|
||||
// November 1, 2026 05:59:59 UTC = 01:59:59 EDT (1 second before fall back)
|
||||
struct tm before {};
|
||||
before.tm_sec = 59;
|
||||
before.tm_min = 59;
|
||||
before.tm_hour = 5;
|
||||
before.tm_mday = 1;
|
||||
before.tm_mon = 10;
|
||||
before.tm_year = 126;
|
||||
time_t before_epoch = internal::tm_to_epoch_utc(&before);
|
||||
assert(is_in_dst(before_epoch, tz) == true);
|
||||
|
||||
// November 1, 2026 06:00:00 UTC = 02:00:00 EDT -> 01:00:00 EST (DST ended)
|
||||
struct tm after {};
|
||||
after.tm_sec = 0;
|
||||
after.tm_min = 0;
|
||||
after.tm_hour = 6;
|
||||
after.tm_mday = 1;
|
||||
after.tm_mon = 10;
|
||||
after.tm_year = 126;
|
||||
time_t after_epoch = internal::tm_to_epoch_utc(&after);
|
||||
assert(is_in_dst(after_epoch, tz) == false);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main
|
||||
// ============================================================================
|
||||
|
||||
int main() {
|
||||
printf("POSIX TZ Parser Unit Tests\n");
|
||||
printf("==========================\n\n");
|
||||
|
||||
printf("Basic TZ string parsing:\n");
|
||||
RUN_TEST(parse_simple_offset_est5);
|
||||
RUN_TEST(parse_negative_offset_cet);
|
||||
RUN_TEST(parse_explicit_positive_offset);
|
||||
RUN_TEST(parse_zero_offset);
|
||||
RUN_TEST(parse_us_eastern_with_dst);
|
||||
RUN_TEST(parse_us_central_with_time);
|
||||
RUN_TEST(parse_europe_berlin);
|
||||
RUN_TEST(parse_new_zealand);
|
||||
RUN_TEST(parse_explicit_dst_offset);
|
||||
|
||||
printf("\nAngle-bracket notation (espressif/newlib-esp32#8):\n");
|
||||
RUN_TEST(parse_angle_bracket_positive);
|
||||
RUN_TEST(parse_angle_bracket_negative);
|
||||
RUN_TEST(parse_angle_bracket_with_dst);
|
||||
RUN_TEST(parse_angle_bracket_named);
|
||||
RUN_TEST(parse_angle_bracket_with_minutes);
|
||||
|
||||
printf("\nHalf-hour and unusual offsets:\n");
|
||||
RUN_TEST(parse_offset_with_minutes_india);
|
||||
RUN_TEST(parse_offset_with_minutes_nepal);
|
||||
RUN_TEST(parse_offset_with_seconds);
|
||||
RUN_TEST(parse_chatham_islands);
|
||||
|
||||
printf("\nInvalid input handling:\n");
|
||||
RUN_TEST(parse_empty_string_fails);
|
||||
RUN_TEST(parse_null_fails);
|
||||
RUN_TEST(parse_short_name_fails);
|
||||
RUN_TEST(parse_missing_offset_fails);
|
||||
RUN_TEST(parse_unterminated_bracket_fails);
|
||||
|
||||
printf("\nJ-format and plain day number:\n");
|
||||
RUN_TEST(parse_j_format_basic);
|
||||
RUN_TEST(parse_j_format_with_time);
|
||||
RUN_TEST(parse_plain_day_number);
|
||||
RUN_TEST(parse_plain_day_number_with_time);
|
||||
RUN_TEST(j_format_invalid_day_zero);
|
||||
RUN_TEST(j_format_invalid_day_366);
|
||||
RUN_TEST(plain_day_invalid_day_366);
|
||||
|
||||
printf("\nJulian day to month/day conversion:\n");
|
||||
RUN_TEST(julian_day_60_is_march_1);
|
||||
RUN_TEST(julian_day_1_is_jan_1);
|
||||
RUN_TEST(julian_day_365_is_dec_31);
|
||||
RUN_TEST(day_of_year_59_differs_by_leap);
|
||||
RUN_TEST(day_of_year_0_is_jan_1);
|
||||
|
||||
printf("\nDay of week calculation:\n");
|
||||
RUN_TEST(day_of_week_known_dates);
|
||||
RUN_TEST(leap_year_detection);
|
||||
RUN_TEST(days_in_month_regular);
|
||||
RUN_TEST(days_in_month_leap_year);
|
||||
|
||||
printf("\nDST transition calculation:\n");
|
||||
RUN_TEST(dst_start_us_eastern_2026);
|
||||
RUN_TEST(dst_end_us_eastern_2026);
|
||||
RUN_TEST(last_sunday_of_march_2026);
|
||||
RUN_TEST(last_sunday_of_october_2026);
|
||||
RUN_TEST(first_sunday_of_april_2026);
|
||||
|
||||
printf("\nis_in_dst tests:\n");
|
||||
RUN_TEST(is_in_dst_us_eastern_summer);
|
||||
RUN_TEST(is_in_dst_us_eastern_winter);
|
||||
RUN_TEST(is_in_dst_no_dst_timezone);
|
||||
RUN_TEST(southern_hemisphere_dst_summer);
|
||||
RUN_TEST(southern_hemisphere_dst_winter);
|
||||
|
||||
printf("\nepoch_to_local_tm tests:\n");
|
||||
RUN_TEST(epoch_to_local_basic);
|
||||
RUN_TEST(epoch_to_local_with_offset);
|
||||
RUN_TEST(epoch_to_local_dst_transition);
|
||||
|
||||
printf("\nDST boundary edge cases:\n");
|
||||
RUN_TEST(dst_boundary_just_before_spring_forward);
|
||||
RUN_TEST(dst_boundary_just_before_fall_back);
|
||||
|
||||
printf("\nVerification against libc:\n");
|
||||
RUN_TEST(verify_us_eastern_multiple_epochs);
|
||||
RUN_TEST(verify_us_central_multiple_epochs);
|
||||
RUN_TEST(verify_europe_berlin_multiple_epochs);
|
||||
RUN_TEST(verify_angle_bracket_notation);
|
||||
RUN_TEST(verify_india_half_hour);
|
||||
RUN_TEST(verify_new_zealand_southern_hemisphere);
|
||||
RUN_TEST(verify_australia_sydney);
|
||||
|
||||
printf("\n==========================\n");
|
||||
printf("All tests passed!\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user