Merge pull request #10422 from Icinga/mktime-dst-consistency

Ensure consistent mktime() DST behavior across different implementations
This commit is contained in:
Julian Brost 2025-04-30 16:51:05 +02:00 committed by GitHub
commit b2b47981a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 249 additions and 164 deletions

View File

@ -14,11 +14,6 @@ case "$DISTRO" in
# https://gitlab.alpinelinux.org/alpine/aports/-/blob/master/community/icinga2/APKBUILD
apk add bison boost-dev ccache cmake flex g++ libedit-dev libressl-dev ninja-build tzdata
ln -vs /usr/lib/ninja-build/bin/ninja /usr/local/bin/ninja
# This test fails due to some glibc/musl mismatch regarding timezone PST/PDT.
# - https://www.openwall.com/lists/musl/2024/03/05/2
# - https://gitlab.alpinelinux.org/alpine/aports/-/blob/b3ea02e2251451f9511086e1970f21eb640097f7/community/icinga2/disable-failing-tests.patch
sed -i '/icinga_legacytimeperiod\/dst$/d' /icinga2/test/CMakeLists.txt
;;
amazonlinux:2)

View File

@ -35,7 +35,7 @@ DateTime::DateTime(const std::vector<Value>& args)
tms.tm_isdst = -1;
m_Value = mktime(&tms);
m_Value = Utility::TmToTimestamp(&tms);
} else if (args.size() == 1)
m_Value = args[0];
else

View File

@ -1958,3 +1958,51 @@ bool Utility::ComparePasswords(const String& enteredPassword, const String& actu
return result;
}
/**
* Normalizes the given struct tm like mktime() from libc does with some exception for DST handling: If the given time
* exists twice on a day, the instance in the DST timezone is picked. If the time does not actually exist on a day, it's
* interpreted using the UTC offset of the standard timezone and then normalized.
*
* This is done in order to provide consistent behavior across operating systems. Historically, Icinga 2 just relied on
* whatever mktime() of the operating system did and this function mimics what glibc does as that's what most systems
* use.
*
* @param t tm struct to be normalized
* @return time_t representing the timestamp given by t
*/
time_t Utility::NormalizeTm(tm *t)
{
// If tm_isdst already specifies the timezone (0 or 1), just use the mktime() behavior.
if (t->tm_isdst >= 0) {
return mktime(t);
}
const tm copy = *t;
t->tm_isdst = 1;
time_t result = mktime(t);
if (result != -1 && t->tm_isdst == 1) {
return result;
}
// Restore the original input. mktime() can (and does) change more fields than just tm_isdst by converting from
// daylight saving time to standard time (it moves the contents by (typically) an hour, which can move across
// days/weeks/months/years changing all other fields).
*t = copy;
t->tm_isdst = 0;
return mktime(t);
}
/**
* Returns the same as NormalizeTm() but takes a const pointer as argument and thus does not modify it.
*
* @param t struct tm to convert to time_t
* @return time_t representing the timestamp given by t
*/
time_t Utility::TmToTimestamp(const tm *t)
{
tm copy = *t;
return NormalizeTm(&copy);
}

View File

@ -185,6 +185,9 @@ public:
return in.SubStr(0, maxLength - sha1HexLength - strlen(trunc)) + trunc + SHA1(in);
}
static time_t NormalizeTm(tm *t);
static time_t TmToTimestamp(const tm *t);
private:
Utility();

View File

@ -13,23 +13,12 @@ using namespace icinga;
REGISTER_FUNCTION_NONCONST(Internal, LegacyTimePeriod, &LegacyTimePeriod::ScriptFunc, "tp:begin:end");
/**
* Returns the same as mktime() but does not modify its argument and takes a const pointer.
*
* @param t struct tm to convert to time_t
* @return time_t representing the timestamp given by t
*/
static time_t mktime_const(const tm *t) {
tm copy = *t;
return mktime(&copy);
}
bool LegacyTimePeriod::IsInTimeRange(const tm *begin, const tm *end, int stride, const tm *reference)
{
time_t tsbegin, tsend, tsref;
tsbegin = mktime_const(begin);
tsend = mktime_const(end);
tsref = mktime_const(reference);
tsbegin = Utility::TmToTimestamp(begin);
tsend = Utility::TmToTimestamp(end);
tsref = Utility::TmToTimestamp(reference);
if (tsref < tsbegin || tsref >= tsend)
return false;
@ -85,7 +74,7 @@ void LegacyTimePeriod::FindNthWeekday(int wday, int n, tm *reference)
t.tm_sec = 0;
t.tm_isdst = -1;
mktime(&t);
Utility::NormalizeTm(&t);
if (t.tm_wday == wday) {
seen++;
@ -398,8 +387,8 @@ bool LegacyTimePeriod::IsInDayDefinition(const String& daydef, const tm *referen
ParseTimeRange(daydef, &begin, &end, &stride, reference);
Log(LogDebug, "LegacyTimePeriod")
<< "ParseTimeRange: '" << daydef << "' => " << mktime(&begin)
<< " -> " << mktime(&end) << ", stride: " << stride;
<< "ParseTimeRange: '" << daydef << "' => " << Utility::TmToTimestamp(&begin)
<< " -> " << Utility::TmToTimestamp(&end) << ", stride: " << stride;
return IsInTimeRange(&begin, &end, stride, reference);
}
@ -448,8 +437,8 @@ Dictionary::Ptr LegacyTimePeriod::ProcessTimeRange(const String& timestamp, cons
ProcessTimeRangeRaw(timestamp, reference, &begin, &end);
return new Dictionary({
{ "begin", (long)mktime(&begin) },
{ "end", (long)mktime(&end) }
{ "begin", (long)Utility::TmToTimestamp(&begin) },
{ "end", (long)Utility::TmToTimestamp(&end) }
});
}
@ -480,13 +469,13 @@ Dictionary::Ptr LegacyTimePeriod::FindRunningSegment(const String& daydef, const
time_t tsend, tsiter, tsref;
int stride;
tsref = mktime_const(reference);
tsref = Utility::TmToTimestamp(reference);
ParseTimeRange(daydef, &begin, &end, &stride, reference);
iter = begin;
tsend = mktime(&end);
tsend = Utility::NormalizeTm(&end);
do {
if (IsInTimeRange(&begin, &end, stride, &iter)) {
@ -518,7 +507,7 @@ Dictionary::Ptr LegacyTimePeriod::FindRunningSegment(const String& daydef, const
iter.tm_hour = 0;
iter.tm_min = 0;
iter.tm_sec = 0;
tsiter = mktime(&iter);
tsiter = Utility::NormalizeTm(&iter);
} while (tsiter < tsend);
return nullptr;
@ -538,13 +527,13 @@ Dictionary::Ptr LegacyTimePeriod::FindNextSegment(const String& daydef, const St
ref.tm_mday++;
}
tsref = mktime(&ref);
tsref = Utility::NormalizeTm(&ref);
ParseTimeRange(daydef, &begin, &end, &stride, &ref);
iter = begin;
tsend = mktime(&end);
tsend = Utility::NormalizeTm(&end);
do {
if (IsInTimeRange(&begin, &end, stride, &iter)) {
@ -575,7 +564,7 @@ Dictionary::Ptr LegacyTimePeriod::FindNextSegment(const String& daydef, const St
iter.tm_hour = 0;
iter.tm_min = 0;
iter.tm_sec = 0;
tsiter = mktime(&iter);
tsiter = Utility::NormalizeTm(&iter);
} while (tsiter < tsend);
}
@ -607,17 +596,17 @@ Array::Ptr LegacyTimePeriod::ScriptFunc(const TimePeriod::Ptr& tp, double begin,
t->tm_isdst = -1;
// Normalize fields using mktime.
mktime(t);
Utility::NormalizeTm(t);
// Reset tm_isdst so that future calls figure out the correct time zone after setting tm_hour/tm_min/tm_sec.
t->tm_isdst = -1;
};
for (tm reference = tm_begin; mktime_const(&reference) <= end; advance_to_next_day(&reference)) {
for (tm reference = tm_begin; Utility::TmToTimestamp(&reference) <= end; advance_to_next_day(&reference)) {
#ifdef I2_DEBUG
Log(LogDebug, "LegacyTimePeriod")
<< "Checking reference time " << mktime_const(&reference);
<< "Checking reference time " << Utility::TmToTimestamp(&reference);
#endif /* I2_DEBUG */
ObjectLock olock(ranges);

View File

@ -57,6 +57,7 @@ add_boost_test(types
set(base_test_SOURCES
icingaapplication-fixture.cpp
utils.cpp
base-array.cpp
base-base64.cpp
base-convert.cpp
@ -185,6 +186,7 @@ add_boost_test(base
base_utility/EscapeCreateProcessArg
base_utility/TruncateUsingHash
base_utility/FormatDateTime
base_utility/NormalizeTm
base_value/scalar
base_value/convert
base_value/format

View File

@ -1,6 +1,7 @@
/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
#include "base/utility.hpp"
#include "test/utils.hpp"
#include <chrono>
#include <BoostTestTargetConfig.h>
@ -230,4 +231,86 @@ BOOST_AUTO_TEST_CASE(FormatDateTime) {
BOOST_CHECK_THROW(Utility::FormatDateTime("%Y", positive_out_of_range), positive_overflow);
}
BOOST_AUTO_TEST_CASE(NormalizeTm)
{
GlobalTimezoneFixture tz(GlobalTimezoneFixture::TestTimezoneWithDST);
auto normalize = [](const std::string_view& input) {
tm t = make_tm(std::string(input));
return Utility::NormalizeTm(&t);
};
auto is_dst = [](const std::string_view& input) {
tm t = make_tm(std::string(input));
Utility::NormalizeTm(&t);
BOOST_CHECK_GE(t.tm_isdst, 0);
return t.tm_isdst > 0;
};
// The whole day 2021-01-01 uses PST (24h day)
BOOST_CHECK(!is_dst("2021-01-01 10:00:00"));
BOOST_CHECK_EQUAL(normalize("2021-01-01 10:00:00"), 1609524000);
BOOST_CHECK_EQUAL(normalize("2021-01-01 10:00:00 PST"), 1609524000);
BOOST_CHECK_EQUAL(normalize("2021-01-01 11:00:00 PDT"), 1609524000); // normalized to 10:00 PST
BOOST_CHECK_EQUAL(normalize("2021-01-02 00:00:00") - normalize("2021-01-01 00:00:00"), 24*60*60);
// The whole day 2021-07-01 uses PDT (24h day)
BOOST_CHECK(is_dst("2021-07-01 10:00:00"));
BOOST_CHECK_EQUAL(normalize("2021-07-01 10:00:00"), 1625158800);
BOOST_CHECK_EQUAL(normalize("2021-07-01 10:00:00 PDT"), 1625158800);
BOOST_CHECK_EQUAL(normalize("2021-07-01 09:00:00 PST"), 1625158800); // normalized to 10:00 PDT
BOOST_CHECK_EQUAL(normalize("2021-07-02 00:00:00") - normalize("2021-07-01 00:00:00"), 24*60*60);
// On 2021-03-14, PST changes to PDT (23h day)
BOOST_CHECK(!is_dst("2021-03-14 00:00:00"));
BOOST_CHECK(is_dst("2021-03-14 23:59:59"));
BOOST_CHECK_EQUAL(normalize("2021-03-15 00:00:00") - normalize("2021-03-14 00:00:00"), 23*60*60);
BOOST_CHECK_EQUAL(normalize("2021-03-14 01:59:59 PST"), 1615715999);
// The following three times do not exist on that day in that timezone.
// They are interpreted as UTC-8, which is the offset of PST.
BOOST_CHECK_EQUAL(normalize("2021-03-14 02:00:00 PST"), 1615716000);
BOOST_CHECK_EQUAL(normalize("2021-03-14 02:30:00 PST"), 1615717800);
BOOST_CHECK_EQUAL(normalize("2021-03-14 03:00:00 PST"), 1615719600);
BOOST_CHECK_EQUAL(normalize("2021-03-14 03:00:00 PDT"), 1615716000);
// The following three times do not exist on that day in that timezone.
// They are interpreted as UTC-7, which is the offset of PDT.
BOOST_CHECK_EQUAL(normalize("2021-03-14 01:59:59 PDT"), 1615712399);
BOOST_CHECK_EQUAL(normalize("2021-03-14 02:00:00 PDT"), 1615712400);
BOOST_CHECK_EQUAL(normalize("2021-03-14 02:30:00 PDT"), 1615714200);
BOOST_CHECK_EQUAL(normalize("2021-03-14 01:59:59"), 1615715999);
BOOST_CHECK_EQUAL(normalize("2021-03-14 03:00:00"), 1615716000);
// The following two times don't exist on that day, they are within the hour that is skipped.
// They are interpreted as UTC-8 (offset of PST) and then normalized to PDT.
BOOST_CHECK_EQUAL(normalize("2021-03-14 02:00:00"), 1615716000);
BOOST_CHECK_EQUAL(normalize("2021-03-14 02:30:00"), 1615717800);
// On 2021-11-07, PDT changes to PST (25h day)
BOOST_CHECK(is_dst("2021-11-07 00:00:00"));
BOOST_CHECK(!is_dst("2021-11-07 23:59:59"));
BOOST_CHECK_EQUAL(normalize("2021-11-08 00:00:00") - normalize("2021-11-07 00:00:00"), 25*60*60);
BOOST_CHECK_EQUAL(normalize("2021-11-07 00:59:59 PDT"), 1636271999);
BOOST_CHECK_EQUAL(normalize("2021-11-07 01:00:00 PDT"), 1636272000);
BOOST_CHECK_EQUAL(normalize("2021-11-07 01:30:00 PDT"), 1636273800);
BOOST_CHECK_EQUAL(normalize("2021-11-07 01:59:59 PDT"), 1636275599);
// The following time does not exist on that day in that timezone, it's interpreted as 01:00:00 PST.
BOOST_CHECK_EQUAL(normalize("2021-11-07 02:00:00 PDT"), 1636275600);
// The following time does not exist on that day in that timezone, it's interpreted as 01:59:59 PDT.
BOOST_CHECK_EQUAL(normalize("2021-11-07 00:59:59 PST"), 1636275599);
BOOST_CHECK_EQUAL(normalize("2021-11-07 01:00:00 PST"), 1636275600);
BOOST_CHECK_EQUAL(normalize("2021-11-07 01:30:00 PST"), 1636277400);
BOOST_CHECK_EQUAL(normalize("2021-11-07 01:59:59 PST"), 1636279199);
BOOST_CHECK_EQUAL(normalize("2021-11-07 02:00:00 PST"), 1636279200);
BOOST_CHECK_EQUAL(normalize("2021-11-07 00:59:59"), 1636271999); // unambiguous: PDT
BOOST_CHECK_EQUAL(normalize("2021-11-07 01:00:00"), 1636272000); // exists twice, interpreted as PDT
BOOST_CHECK_EQUAL(normalize("2021-11-07 01:30:00"), 1636273800); // exists twice, interpreted as PDT
BOOST_CHECK_EQUAL(normalize("2021-11-07 01:59:59"), 1636275599); // exists twice, interpreted as PDT
BOOST_CHECK_EQUAL(normalize("2021-11-07 02:00:00"), 1636279200); // unambiguous: PST
}
BOOST_AUTO_TEST_SUITE_END()

View File

@ -2,6 +2,7 @@
#include "base/utility.hpp"
#include "icinga/legacytimeperiod.hpp"
#include "test/utils.hpp"
#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/date_time/posix_time/ptime.hpp>
#include <boost/date_time/posix_time/posix_time_duration.hpp>
@ -15,52 +16,8 @@ using namespace icinga;
BOOST_AUTO_TEST_SUITE(icinga_legacytimeperiod);
struct GlobalTimezoneFixture
{
char *tz;
GlobalTimezoneFixture(const char *fixed_tz = "")
{
tz = getenv("TZ");
#ifdef _WIN32
_putenv_s("TZ", fixed_tz == "" ? "UTC" : fixed_tz);
#else
setenv("TZ", fixed_tz, 1);
#endif
tzset();
}
~GlobalTimezoneFixture()
{
#ifdef _WIN32
if (tz)
_putenv_s("TZ", tz);
else
_putenv_s("TZ", "");
#else
if (tz)
setenv("TZ", tz, 1);
else
unsetenv("TZ");
#endif
tzset();
}
};
BOOST_GLOBAL_FIXTURE(GlobalTimezoneFixture);
// DST changes in America/Los_Angeles:
// 2021-03-14: 01:59:59 PST (UTC-8) -> 03:00:00 PDT (UTC-7)
// 2021-11-07: 01:59:59 PDT (UTC-7) -> 01:00:00 PST (UTC-8)
#ifndef _WIN32
static const char *dst_test_timezone = "America/Los_Angeles";
#else /* _WIN32 */
// Tests are using pacific time because Windows only really supports timezones following US DST rules with the TZ
// environment variable. Format is "[Standard TZ][negative UTC offset][DST TZ]".
// https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/tzset?view=msvc-160#remarks
static const char *dst_test_timezone = "PST8PDT";
#endif /* _WIN32 */
BOOST_AUTO_TEST_CASE(simple)
{
tm tm_beg, tm_end, tm_ref;
@ -471,35 +428,6 @@ BOOST_AUTO_TEST_CASE(advanced)
AdvancedHelper("09:00:03-30:00:04", {{2014, 9, 24}, {9, 0, 3}}, {{2014, 9, 25}, {6, 0, 4}});
}
tm make_tm(std::string s)
{
int dst = -1;
size_t l = strlen("YYYY-MM-DD HH:MM:SS");
if (s.size() > l) {
std::string zone = s.substr(l);
if (zone == " PST") {
dst = 0;
} else if (zone == " PDT") {
dst = 1;
} else {
// tests should only use PST/PDT (for now)
BOOST_CHECK_MESSAGE(false, "invalid or unknown time time: " << zone);
}
}
std::tm t = {};
#if defined(__GNUC__) && __GNUC__ < 5
// GCC did not implement std::get_time() until version 5
strptime(s.c_str(), "%Y-%m-%d %H:%M:%S", &t);
#else /* defined(__GNUC__) && __GNUC__ < 5 */
std::istringstream stream(s);
stream >> std::get_time(&t, "%Y-%m-%d %H:%M:%S");
#endif /* defined(__GNUC__) && __GNUC__ < 5 */
t.tm_isdst = dst;
return t;
}
time_t make_time_t(const tm* t)
{
tm copy = *t;
@ -551,7 +479,7 @@ std::ostream& operator<<(std::ostream& o, const boost::optional<Segment>& s)
BOOST_AUTO_TEST_CASE(dst)
{
GlobalTimezoneFixture tz(dst_test_timezone);
GlobalTimezoneFixture tz(GlobalTimezoneFixture::TestTimezoneWithDST);
// Self-tests for helper functions
BOOST_CHECK_EQUAL(make_tm("2021-11-07 02:30:00").tm_isdst, -1);
@ -589,13 +517,8 @@ BOOST_AUTO_TEST_CASE(dst)
day, "01:30-02:30",
{make_tm("2021-03-14 01:00:00 PST")},
{make_tm("2021-03-14 01:59:59 PST")},
#ifndef _WIN32
// As 02:30 does not exist on this day, it is parsed as if it was 02:30 PST which is actually 03:30 PDT.
Segment("2021-03-14 01:30:00 PST", "2021-03-14 03:30:00 PDT"),
#else
// Windows interpretes 02:30 as 01:30 PST, so it is an empty segment.
boost::none,
#endif
});
}
@ -605,14 +528,9 @@ BOOST_AUTO_TEST_CASE(dst)
day, "02:30-03:30",
{make_tm("2021-03-14 01:00:00 PST")},
{make_tm("2021-03-14 03:00:00 PDT")},
#ifndef _WIN32
// As 02:30 does not exist on this day, it is parsed as if it was 02:30 PST which is actually 03:30 PDT.
// Therefore, the result is a segment from 03:30 PDT to 03:30 PDT with a duration of 0, i.e. no segment.
boost::none,
#else
// Windows parses non-existing 02:30 as 01:30 PST, resulting in an 1 hour segment.
Segment("2021-03-14 01:30:00 PST", "2021-03-14 03:30:00 PDT"),
#endif
});
}
@ -621,13 +539,8 @@ BOOST_AUTO_TEST_CASE(dst)
day, "02:15-03:45",
{make_tm("2021-03-14 01:00:00 PST")},
{make_tm("2021-03-14 03:30:00 PDT")},
#ifndef _WIN32
// As 02:15 does not exist on this day, it is parsed as if it was 02:15 PST which is actually 03:15 PDT.
Segment("2021-03-14 03:15:00 PDT", "2021-03-14 03:45:00 PDT"),
#else
// Windows interprets 02:15 as 01:15 PST though.
Segment("2021-03-14 01:15:00 PST", "2021-03-14 03:45:00 PDT"),
#endif
});
// range after DST change
@ -659,7 +572,6 @@ BOOST_AUTO_TEST_CASE(dst)
if (day.find("sunday") == std::string::npos) { // skip for non-absolute day specs (would find another sunday)
// range existing twice during DST change (first instance)
#ifndef _WIN32
tests.push_back(TestData{
day, "01:15-01:45",
{make_tm("2021-11-07 01:00:00 PDT")},
@ -667,15 +579,6 @@ BOOST_AUTO_TEST_CASE(dst)
// Duplicate times are interpreted as the first occurrence.
Segment("2021-11-07 01:15:00 PDT", "2021-11-07 01:45:00 PDT"),
});
#else
tests.push_back(TestData{
day, "01:15-01:45",
{make_tm("2021-11-07 01:00:00 PDT")},
{make_tm("2021-11-07 01:30:00 PST")},
// However, Windows always uses the second occurrence.
Segment("2021-11-07 01:15:00 PST", "2021-11-07 01:45:00 PST"),
});
#endif
}
if (day.find("sunday") == std::string::npos) { // skip for non-absolute day specs (would find another sunday)
@ -684,13 +587,8 @@ BOOST_AUTO_TEST_CASE(dst)
day, "01:15-01:45",
{make_tm("2021-11-07 01:00:00 PST")},
{make_tm("2021-11-07 01:30:00 PST")},
#ifndef _WIN32
// Interpreted as the first occurrence, so it's in the past.
boost::none,
#else
// On Windows, it's the second occurrence, so it's still in the present/future and is found.
Segment("2021-11-07 01:15:00 PST", "2021-11-07 01:45:00 PST"),
#endif
});
}
@ -716,13 +614,8 @@ BOOST_AUTO_TEST_CASE(dst)
day, "00:30-01:30",
{make_tm("2021-11-07 00:00:00 PDT")},
{make_tm("2021-11-07 01:00:00 PDT")},
#ifndef _WIN32
// Both times are interpreted as the first instance on that day (i.e both PDT).
Segment("2021-11-07 00:30:00 PDT", "2021-11-07 01:30:00 PDT")
#else
// Windows interprets duplicate times as the second instance (i.e. both PST).
Segment("2021-11-07 00:30:00 PDT", "2021-11-07 01:30:00 PST")
#endif
});
// range beginning during duplicate DST hour (first instance)
@ -730,18 +623,12 @@ BOOST_AUTO_TEST_CASE(dst)
day, "01:30-02:30",
{make_tm("2021-11-07 01:00:00 PDT")},
{make_tm("2021-11-07 02:00:00 PST")},
#ifndef _WIN32
// 01:30 is interpreted as the first occurrence (PDT) but since there's no 02:30 PDT, it's PST.
Segment("2021-11-07 01:30:00 PDT", "2021-11-07 02:30:00 PST")
#else
// Windows interprets both as PST though.
Segment("2021-11-07 01:30:00 PST", "2021-11-07 02:30:00 PST")
#endif
});
if (day.find("sunday") == std::string::npos) { // skip for non-absolute day specs (would find another sunday)
// range ending during duplicate DST hour (second instance)
#ifndef _WIN32
tests.push_back(TestData{
day, "00:30-01:30",
{make_tm("2021-11-07 00:00:00 PST")},
@ -750,15 +637,6 @@ BOOST_AUTO_TEST_CASE(dst)
// 01:00 PST (02:00 PDT) is after the segment.
boost::none,
});
#else
tests.push_back(TestData{
day, "00:30-01:30",
{make_tm("2021-11-07 00:00:00 PDT")},
{make_tm("2021-11-07 01:00:00 PST")},
// As Windows interprets the end as PST, it's still in the future and the segment is found.
Segment("2021-11-07 00:30:00 PDT", "2021-11-07 01:30:00 PST"),
});
#endif
}
// range beginning during duplicate DST hour (second instance)
@ -766,13 +644,8 @@ BOOST_AUTO_TEST_CASE(dst)
day, "01:30-02:30",
{make_tm("2021-11-07 01:00:00 PDT")},
{make_tm("2021-11-07 02:00:00 PST")},
#ifndef _WIN32
// As 01:30 always refers to the first occurrence (PDT), this is actually a 2 hour segment.
Segment("2021-11-07 01:30:00 PDT", "2021-11-07 02:30:00 PST"),
#else
// On Windows, it refers t the second occurrence (PST), therefore it's an 1 hour segment.
Segment("2021-11-07 01:30:00 PST", "2021-11-07 02:30:00 PST"),
#endif
});
}
@ -830,7 +703,7 @@ BOOST_AUTO_TEST_CASE(dst)
// This tests checks that TimePeriod::IsInside() always returns true for a 24x7 period, even around DST changes.
BOOST_AUTO_TEST_CASE(dst_isinside)
{
GlobalTimezoneFixture tz(dst_test_timezone);
GlobalTimezoneFixture tz(GlobalTimezoneFixture::TestTimezoneWithDST);
Function::Ptr update = new Function("LegacyTimePeriod", LegacyTimePeriod::ScriptFunc, {"tp", "begin", "end"});
Dictionary::Ptr ranges = new Dictionary({

67
test/utils.cpp Normal file
View File

@ -0,0 +1,67 @@
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
#include "utils.hpp"
#include <cstring>
#include <iomanip>
#include <sstream>
#include <boost/test/unit_test.hpp>
tm make_tm(std::string s)
{
int dst = -1;
size_t l = strlen("YYYY-MM-DD HH:MM:SS");
if (s.size() > l) {
std::string zone = s.substr(l);
if (zone == " PST") {
dst = 0;
} else if (zone == " PDT") {
dst = 1;
} else {
// tests should only use PST/PDT (for now)
BOOST_CHECK_MESSAGE(false, "invalid or unknown time time: " << zone);
}
}
std::tm t = {};
std::istringstream stream(s);
stream >> std::get_time(&t, "%Y-%m-%d %H:%M:%S");
t.tm_isdst = dst;
return t;
}
#ifndef _WIN32
const char *GlobalTimezoneFixture::TestTimezoneWithDST = "America/Los_Angeles";
#else /* _WIN32 */
// Tests are using pacific time because Windows only really supports timezones following US DST rules with the TZ
// environment variable. Format is "[Standard TZ][negative UTC offset][DST TZ]".
// https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/tzset?view=msvc-160#remarks
const char *GlobalTimezoneFixture::TestTimezoneWithDST = "PST8PDT";
#endif /* _WIN32 */
GlobalTimezoneFixture::GlobalTimezoneFixture(const char *fixed_tz)
{
tz = getenv("TZ");
#ifdef _WIN32
_putenv_s("TZ", fixed_tz == "" ? "UTC" : fixed_tz);
#else
setenv("TZ", fixed_tz, 1);
#endif
tzset();
}
GlobalTimezoneFixture::~GlobalTimezoneFixture()
{
#ifdef _WIN32
if (tz)
_putenv_s("TZ", tz);
else
_putenv_s("TZ", "");
#else
if (tz)
setenv("TZ", tz, 1);
else
unsetenv("TZ");
#endif
tzset();
}

25
test/utils.hpp Normal file
View File

@ -0,0 +1,25 @@
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
#pragma once
#include <ctime>
#include <string>
tm make_tm(std::string s);
struct GlobalTimezoneFixture
{
/**
* Timezone used for testing DST changes.
*
* DST changes in America/Los_Angeles:
* 2021-03-14: 01:59:59 PST (UTC-8) -> 03:00:00 PDT (UTC-7)
* 2021-11-07: 01:59:59 PDT (UTC-7) -> 01:00:00 PST (UTC-8)
*/
static const char *TestTimezoneWithDST;
GlobalTimezoneFixture(const char *fixed_tz = "");
~GlobalTimezoneFixture();
char *tz;
};