diff --git a/libc/src/time/CMakeLists.txt b/libc/src/time/CMakeLists.txt --- a/libc/src/time/CMakeLists.txt +++ b/libc/src/time/CMakeLists.txt @@ -8,6 +8,23 @@ libc.include.errno libc.include.time libc.src.errno.errno + .time_zone_posix +) + +add_object_library( + time_zone_posix + CXX_STANDARD 20 + SRCS + time_zone_posix.cpp + HDRS + time_zone_posix.h + DEPENDS + libc.src.ctype.isdigit + libc.src.stdlib.strtol + libc.src.string.memory_utils.memset_implementation + libc.src.string.strchr + libc.src.__support.CPP.optional + libc.src.__support.str_to_integer ) add_entrypoint_object( diff --git a/libc/src/time/time_utils.h b/libc/src/time/time_utils.h --- a/libc/src/time/time_utils.h +++ b/libc/src/time/time_utils.h @@ -21,11 +21,28 @@ namespace __llvm_libc { namespace time_utils { +enum Month : int { + JANUARY, + FEBRUARY, + MARCH, + APRIL, + MAY, + JUNE, + JULY, + AUGUST, + SEPTEMBER, + OCTOBER, + NOVEMBER, + DECEMBER +}; + struct TimeConstants { static constexpr int SECONDS_PER_MIN = 60; static constexpr int SECONDS_PER_HOUR = 3600; static constexpr int SECONDS_PER_DAY = 86400; + static constexpr int MINUTES_PER_HOUR = 60; static constexpr int DAYS_PER_WEEK = 7; + static constexpr int MAXIMUM_WEEKS_PER_MONTH = 5; static constexpr int MONTHS_PER_YEAR = 12; static constexpr int DAYS_PER_NON_LEAP_YEAR = 365; static constexpr int DAYS_PER_LEAP_YEAR = 366; diff --git a/libc/src/time/time_zone_posix.h b/libc/src/time/time_zone_posix.h new file mode 100644 --- /dev/null +++ b/libc/src/time/time_zone_posix.h @@ -0,0 +1,201 @@ +//===-- Collection of utils for TZ pasrsing ---------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_LIBC_SRC_TIME_TIME_ZONE_POSIX_H +#define LLVM_LIBC_SRC_TIME_TIME_ZONE_POSIX_H + +#include // int8_t, int16_t and int32_t + +#include "src/__support/CPP/optional.h" +#include "src/__support/CPP/string_view.h" + +#include // int8_t, int16_t and int32_t + +namespace __llvm_libc { +namespace time_zone_posix { + +// This enum is used to handle + or - symbol in the offset specification of +// TZ, which is of the fromat "[+|-]hh[:mm[:ss]]". +// If specification says +, then use the TZOffset as is. +// If specification says -, then reverse the TZOffset, +// if TZOffset is -1, then use +1 otherwise use +1 as the multiplier. +// +// TZOffset is used in numeric calculations, thus we couldn't define it as enum +// class. +enum TZOffset { NEGATIVE = -1, POSITIVE = 1 }; + +// The TZ environment variable is specified in +// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html +// +// The following is an example of how the TZ spec is parsed and saved in the +// PosixTimeZone object. +// +// The current POSIX spec for America/Los_Angeles is "PST8PDT,M3.2.0,M11.1.0", +// which would be broken down as ... +// +// PosixTimeZone { +// std_abbr = "PST" +// std_offset = -28800 +// dst_abbr = "PDT" +// dst_offset = -25200 +// dst_start = PosixTransition { +// date { +// m { +// month = 3 +// week = 2 +// weekday = 0 +// } +// } +// time { +// offset = 7200 +// } +// } +// dst_end = PosixTransition { +// date { +// m { +// month = 11 +// week = 1 +// weekday = 0 +// } +// } +// time { +// offset = 7200 +// } +// } +// } +// +// The date/time of the transition. The date is specified as either: +// (J) the Nth day of the year (1 <= N <= 365), excluding leap days, or +// (N) the Nth day of the year (0 <= N <= 365), including leap days, or +// (M) the Nth weekday of a month (e.g., the 2nd Sunday in March). +// The time, specified as a day offset, identifies the particular moment +// of the transition, and may be negative or >= 24h, and in which case +// it would take us to another day, and perhaps week, or even month. + +class PosixTransition { +public: + enum class DateFormat : int { J, N, M }; + + struct Date { + struct NonLeapDay { + int16_t day; // day of non-leap year [1:365] + }; + struct Day { + int16_t day; // day of year [0:365] + }; + struct MonthWeekWeekday { + int8_t month; // month of year [1:12] + int8_t week; // week of month [1:5] (5==last) + int8_t weekday; // 0==Sun, ..., 6=Sat + }; + + DateFormat fmt; + + // TODO(rtenneti): convert the following to libc variant. + union { + NonLeapDay j; + Day n; + MonthWeekWeekday m; + }; + }; + + struct Time { + int32_t offset; // seconds before/after 00:00:00 + }; + + Date date; + Time time; + + PosixTransition() { + date.fmt = DateFormat::N; + date.j.day = 0; + time.offset = 0; + } + + explicit PosixTransition(DateFormat fmt, int16_t day, int32_t offset) { + date.fmt = fmt; + date.j.day = day; + time.offset = offset; + } + + explicit PosixTransition(DateFormat fmt, int8_t month, int8_t week, + int8_t weekday, int32_t offset) { + date.fmt = fmt; + date.m.month = month; + date.m.week = week; + date.m.weekday = weekday; + time.offset = offset; + } +}; + +// The entirety of a POSIX-string specified time-zone rule. The standard +// abbreviation and offset are always given. If the time zone includes +// daylight saving, then the daylight abbreviation is non-empty and the +// remaining fields are also valid. Note that the start/end transitions +// are not ordered---in the southern hemisphere the transition to end +// daylight time occurs first in any particular year. +class PosixTimeZone { +public: + // Default constructor for testing. + PosixTimeZone() + : spec(""), std_abbr("UTC"), std_offset(0), dst_abbr(""), dst_offset(0) {} + + explicit PosixTimeZone(const cpp::string_view &spec) + : spec(spec), std_abbr("UTC"), std_offset(0), dst_abbr(""), + dst_offset(0) {} + + explicit PosixTimeZone(const cpp::string_view &spec, + cpp::string_view std_abbr, int32_t std_offset, + cpp::string_view dst_abbr, int32_t dst_offset, + time_zone_posix::PosixTransition dst_start, + time_zone_posix::PosixTransition dst_end) + : spec(spec), std_abbr(std_abbr), std_offset(std_offset), + dst_abbr(dst_abbr), dst_offset(dst_offset), dst_start(dst_start), + dst_end(dst_end) {} + + // Breaks down a POSIX time-zone specification into its constituent pieces, + // filling in any missing values (DST offset, or start/end transition times) + // with the standard-defined defaults. Returns false if the specification + // could not be parsed (although some fields of *res may have been altered). + static cpp::optional + ParsePosixSpec(const cpp::string_view spec); + + cpp::string_view spec; + + cpp::string_view std_abbr; + int32_t std_offset; + + cpp::string_view dst_abbr; + int32_t dst_offset; + + PosixTransition dst_start; + PosixTransition dst_end; + +private: + bool UpdateStdAbbr(); + bool UpdateDstAbbr(); + bool UpdateStdOffset(); + bool UpdateDstOffset(); + bool UpdateDstStart(); + bool UpdateDstEnd(); + bool SpecHasData(); + + cpp::optional ParseInt(int min, int max); + cpp::optional ParseAbbr(); + cpp::optional ParseOffset(int min_hour, int max_hour, + TZOffset multiplier); + cpp::optional ParseMonthWeekWeekday(); + cpp::optional ParseNonLeapDay(); + cpp::optional ParseLeapDay(); + cpp::optional ParseDateTime(); +}; + +} // namespace time_zone_posix +} // namespace __llvm_libc + +#endif // LLVM_LIBC_SRC_TIME_TIME_ZONE_POSIX_H diff --git a/libc/src/time/time_zone_posix.cpp b/libc/src/time/time_zone_posix.cpp new file mode 100644 --- /dev/null +++ b/libc/src/time/time_zone_posix.cpp @@ -0,0 +1,401 @@ +#include // size_t + +#include "src/ctype/isdigit.h" +#include "src/stdlib/strtol.h" +#include "src/string/strchr.h" +#include "src/time/time_utils.h" +#include "src/time/time_zone_posix.h" + +namespace __llvm_libc { +namespace time_zone_posix { + +using __llvm_libc::time_utils::TimeConstants; + +// https://datatracker.ietf.org/doc/html/rfc8536 +// The hours part of the transition times may be signed and range +// from -167 through 167 (-167 <= hh <= 167) instead of the POSIX- +// required unsigned values from 0 through 24. +static constexpr int MAX_HOURS_IN_TRANSITION_TIMES = 167; + +// TOOD(rtenneti): Make ParseInt be a template and return int8_t, +// unit16_t, etc? That way the static_casts below wouldn't be needed. +// +// Parses int value from the spec. It returns nullopt if the value +// is less than min or greater than max. +cpp::optional PosixTimeZone::ParseInt(int min, int max) { + if (spec.empty()) + return cpp::nullopt; + + char *pEnd; + int value = strtol(spec.data(), &pEnd, 10); // NOLINT(runtime/deprecated_fn) + if (value < min || value > max) + return cpp::nullopt; + + // Delete the number from the input string. + size_t len = pEnd - spec.data(); + spec.remove_prefix(len); + return value; +} + +// abbr = <.*?> | [^-+,\d]{3,} +cpp::optional PosixTimeZone::ParseAbbr() { + if (spec.empty()) + return cpp::nullopt; + + // Handle special zoneinfo <...> form. + cpp::optional abbr; + if (spec.starts_with('<')) { + spec.remove_prefix(1); + const auto pos = spec.find_first_of('>'); + if (pos == cpp::string_view::npos) + return cpp::nullopt; + abbr = spec.substr(0, pos); + // Delete the data up to and including '>' character. + spec.remove_prefix(pos + 1); + return abbr; + } + + size_t len = 0; + // Handle [^-+,\d]{3,} + for (const auto &p : spec) { + if (strchr("-+,", p)) + break; + if (isdigit(p)) + break; + len++; + } + if (len < 3) + return cpp::nullopt; + abbr = spec.substr(0, len); + spec.remove_prefix(len); + return abbr; +} + +// offset = [+|-]hh[:mm[:ss]] (aggregated into single seconds value) +// default_sign_for_offset for std and dst offset is negative. For example, PST +// and PDT have default minus sign for the offset (relative to UTC). In, +// PST8PDT, though 8 doesn't have any sign, but the offset is (-8 * 3600). +cpp::optional +PosixTimeZone::ParseOffset(int min_hour, int max_hour, + TZOffset default_sign_for_offset) { + if (spec.empty()) + return cpp::nullopt; + + // Handle [+|-]. + int multiplier = default_sign_for_offset; + if (spec.starts_with('+') || spec.starts_with('-')) { + if (spec.starts_with('-')) { + // If spec says minus, then we reverse the multiplication_factor otherwise + // we use it as is. + multiplier = (default_sign_for_offset == TZOffset::NEGATIVE) ? 1 : -1; + } + spec.remove_prefix(1); + } + + int hours = 0; + int minutes = 0; + int seconds = 0; + + // Parse hours - hh + cpp::optional result = ParseInt(min_hour, max_hour); + if (!result) + return cpp::nullopt; + hours = result.value(); + + // Check for optional minutes. + // Parse minutes and if there is no data, then default to 0 - hh[:mm] + if (spec.starts_with(':')) { + spec.remove_prefix(1); + + // Parse minutes (minimum is 0 and maximum is 59). + result = ParseInt(0, TimeConstants::MINUTES_PER_HOUR - 1); + if (!result) + return cpp::nullopt; + minutes = result.value(); + + // Check for optional seconds. + // Parse seconds and if there is no data, then default to 0 - hh[:mm[:ss]] + if (spec.starts_with(':')) { + spec.remove_prefix(1); + + // Parse seconds (minimum is 0 and maximum is 59). + result = ParseInt(0, TimeConstants::SECONDS_PER_MIN - 1); + if (!result) + return cpp::nullopt; + seconds = result.value(); + } + } + int32_t offset = + multiplier * ((((hours * TimeConstants::MINUTES_PER_HOUR) + minutes) * + TimeConstants::SECONDS_PER_MIN) + + seconds); + return offset; +} + +// (M) the Nth weekday of a month (e.g., the 2nd Sunday in March). +// Mm.w.d +cpp::optional PosixTimeZone::ParseMonthWeekWeekday() { + // Parse month (minimum is 1 and maximum is 12). + cpp::optional result = ParseInt(1, TimeConstants::MONTHS_PER_YEAR); + if (!result) + return cpp::nullopt; + int month = result.value(); + + // Error if there is no dot. + if (!spec.starts_with('.')) + return cpp::nullopt; + spec.remove_prefix(1); + + // Error if there is no week. + if (spec.empty()) + return cpp::nullopt; + + // Parse week (minimum is 1 and maximum is 5). + result = ParseInt(1, TimeConstants::MAXIMUM_WEEKS_PER_MONTH); + if (!result) + return cpp::nullopt; + int week = result.value(); + + // Error if there is no dot. + if (!spec.starts_with('.')) + return cpp::nullopt; + spec.remove_prefix(1); + + // Error if there is no weekday. + if (spec.empty()) + return cpp::nullopt; + + // Parse Weekday (minimum is 0 and maximum is 6). + result = ParseInt(0, TimeConstants::DAYS_PER_WEEK - 1); + if (!result) + return cpp::nullopt; + int weekday = result.value(); + + PosixTransition posixTransition; + posixTransition.date.fmt = PosixTransition::DateFormat::M; + posixTransition.date.m.month = static_cast(month); + posixTransition.date.m.week = static_cast(week); + posixTransition.date.m.weekday = static_cast(weekday); + return posixTransition; +} + +// Parse Jn +// (J) the Nth day of the year (1 <= N <= 365), excluding leap days. +cpp::optional PosixTimeZone::ParseNonLeapDay() { + const cpp::optional result = + ParseInt(1, TimeConstants::DAYS_PER_NON_LEAP_YEAR); + if (!result) + return cpp::nullopt; + PosixTransition posixTransition; + posixTransition.date.fmt = PosixTransition::DateFormat::J; + posixTransition.date.j.day = static_cast(result.value()); + return posixTransition; +} + +// Parse n +// (N) the Nth day of the year (0 <= N <= 365), including leap days +cpp::optional PosixTimeZone::ParseLeapDay() { + const cpp::optional result = + ParseInt(0, TimeConstants::DAYS_PER_LEAP_YEAR - 1); + if (!result) + return cpp::nullopt; + PosixTransition posixTransition; + posixTransition.date.fmt = PosixTransition::DateFormat::N; + posixTransition.date.n.day = static_cast(result.value()); + return posixTransition; +} + +// datetime = ( Jn | n | Mm.w.d ) [ / offset ] +// The current POSIX spec for America/Los_Angeles is "PST8PDT,M3.2.0,M11.1.0", +// which would be broken down for M11.1.0 as +// PosixTransition { +// date { +// m { +// month = 11 +// week = 1 +// weekday = 0 +// } +// } +// time { +// offset = 7200 +// } +// } +cpp::optional PosixTimeZone::ParseDateTime() { + PosixTransition posixTransition; + if (spec.starts_with(',')) { + spec.remove_prefix(1); + + cpp::optional optionalPosixTransition; + if (spec.starts_with('M')) { + spec.remove_prefix(1); + optionalPosixTransition = ParseMonthWeekWeekday(); // Mm.w.d + } else if (spec.starts_with('J')) { + spec.remove_prefix(1); + optionalPosixTransition = ParseNonLeapDay(); // Jn + } else { + optionalPosixTransition = ParseLeapDay(); // n + } + if (optionalPosixTransition) + posixTransition = optionalPosixTransition.value(); + else + return cpp::nullopt; + } + + // Parse time offset: / offset + posixTransition.time.offset = + 2 * TimeConstants::SECONDS_PER_HOUR; // default offset is 02:00:00 + if (spec.starts_with('/')) { + spec.remove_prefix(1); + // offset value after "/" is always positive. + TZOffset default_sign_for_offset = TZOffset::POSITIVE; + const cpp::optional offset = + ParseOffset(-MAX_HOURS_IN_TRANSITION_TIMES, + MAX_HOURS_IN_TRANSITION_TIMES, default_sign_for_offset); + if (!offset) + return cpp::nullopt; + posixTransition.time.offset = offset.value(); + } + return posixTransition; +} + +// The current POSIX spec for America/Los_Angeles is "PST8PDT,M3.2.0,M11.1.0", +// which would be broken down for std_abbr as "PST". +bool PosixTimeZone::UpdateStdAbbr() { + const auto abbr_result = ParseAbbr(); + if (!abbr_result) + return false; + std_abbr = *abbr_result; + return true; +} + +// The current POSIX spec for America/Los_Angeles is "PST8PDT,M3.2.0,M11.1.0", +// which would be broken down for dst_abbr as "PDT". +bool PosixTimeZone::UpdateDstAbbr() { + const auto abbr_result = ParseAbbr(); + if (!abbr_result) + return false; + dst_abbr = *abbr_result; + return true; +} + +// A zone offset is the difference in hours and minutes between a particular +// time zone and UTC. In ISO 8601, the particular zone offset can be indicated +// in a date or time value. The zone offset can be Z for UTC or it can be a +// value "+" or "-" from UTC. +// +// For the current POSIX spec for America/Los_Angeles is +// "PST8PDT,M3.2.0,M11.1.0", which would be broken down for std_offset would be +// -28800. It is same as -1 * 8 * 3600. 8 is the offset. +bool PosixTimeZone::UpdateStdOffset() { + // offset should be between 0 and 24 hours. + TZOffset default_sign_for_offset = TZOffset::NEGATIVE; + // |default_sign_for_offset| for std and dst offset is negative. For example, + // PST and PDT have default minus sign for the offset (relative to UTC). In, + // PST8PDT, though 8 doesn't have any sign, but the offset is (-8 * 3600). + const cpp::optional offset = + ParseOffset(0, 24, default_sign_for_offset); + if (!offset) + return false; + std_offset = offset.value(); + return true; +} + +// For the current POSIX spec for America/Los_Angeles is +// "PST8PDT,M3.2.0,M11.1.0", which would be broken down for dst_offset as +// -25800. It is same as 3600 + (-1 * 8 * 3600). +bool PosixTimeZone::UpdateDstOffset() { + dst_offset = std_offset + TimeConstants::SECONDS_PER_HOUR; // default + if (!spec.starts_with(',')) { + // offset should be between 0 and 24 hours. + TZOffset default_sign_for_offset = TZOffset::NEGATIVE; + // |default_sign_for_offset| for std and dst offset is negative. For + // example, PST and PDT have default minus sign for the offset (relative to + // UTC). In, PST8PDT, though 8 doesn't have any sign, but the offset is (-8 + // * 3600). + const cpp::optional offset = + ParseOffset(0, 24, default_sign_for_offset); + if (!offset) + return false; + dst_offset = offset.value(); + } + return true; +} + +// The current POSIX spec for America/Los_Angeles is "PST8PDT,M3.2.0,M11.1.0", +// which would be broken down as ... +// dst_start = PosixTransition { +// date { +// m { +// month = 3 +// week = 2 +// weekday = 0 +// } +// } +// time { +// offset = 7200 +// } +// } +bool PosixTimeZone::UpdateDstStart() { + const auto date_time_result = ParseDateTime(); + if (!date_time_result) + return false; + dst_start = *date_time_result; + return true; +} + +// The current POSIX spec for America/Los_Angeles is "PST8PDT,M3.2.0,M11.1.0", +// which would be broken down as ... +// dst_end = PosixTransition { +// date { +// m { +// month = 11 +// week = 1 +// weekday = 0 +// } +// } +// time { +// offset = 7200 +// } +// } +bool PosixTimeZone::UpdateDstEnd() { + const auto date_time_result = ParseDateTime(); + if (!date_time_result) + return false; + dst_end = *date_time_result; + return true; +} + +bool PosixTimeZone::SpecHasData() { + if (spec.empty()) + return false; + return true; +} + +// spec = std offset [ dst [ offset ] , datetime , datetime ] +cpp::optional +PosixTimeZone::ParsePosixSpec(const cpp::string_view spec) { + if (spec.starts_with(':')) + return cpp::nullopt; + + PosixTimeZone res(spec); + if (!res.UpdateStdAbbr()) + return cpp::nullopt; + if (!res.UpdateStdOffset()) + return cpp::nullopt; + if (res.spec.empty()) + return res; + if (!res.UpdateDstAbbr()) + return cpp::nullopt; + if (!res.UpdateDstOffset()) + return cpp::nullopt; + if (!res.UpdateDstStart()) + return cpp::nullopt; + if (!res.UpdateDstEnd()) + return cpp::nullopt; + if (res.spec.size() != 0) + return cpp::nullopt; + return res; +} + +} // namespace time_zone_posix +} // namespace __llvm_libc diff --git a/libc/test/src/time/CMakeLists.txt b/libc/test/src/time/CMakeLists.txt --- a/libc/test/src/time/CMakeLists.txt +++ b/libc/test/src/time/CMakeLists.txt @@ -68,3 +68,15 @@ DEPENDS libc.src.time.mktime ) + +add_libc_unittest( + time_zone_posix + SUITE + libc_time_unittests + SRCS + time_zone_posix_test.cpp + HDRS + TZMatcher.h + DEPENDS + libc.src.time.time_zone_posix +) diff --git a/libc/test/src/time/TZMatcher.h b/libc/test/src/time/TZMatcher.h new file mode 100644 --- /dev/null +++ b/libc/test/src/time/TZMatcher.h @@ -0,0 +1,115 @@ +//===---- TzMatchers.h ------------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_LIBC_TEST_SRC_TIME_TZ_MATCHER_H +#define LLVM_LIBC_TEST_SRC_TIME_TZ_MATCHER_H + +#include "src/time/time_zone_posix.h" + +#include "utils/UnitTest/Test.h" + +namespace __llvm_libc { +namespace tzmatcher { +namespace testing { + +class PosixTimeZoneMatcher + : public __llvm_libc::testing::Matcher { + time_zone_posix::PosixTimeZone expected; + time_zone_posix::PosixTimeZone actual; + +public: + PosixTimeZoneMatcher(time_zone_posix::PosixTimeZone expectedValue) + : expected(expectedValue) {} + + bool posix_transition_equals(time_zone_posix::PosixTransition expectedValue, + time_zone_posix::PosixTransition actualValue) { + switch (expectedValue.date.fmt) { + case time_zone_posix::PosixTransition::DateFormat::J: + return (expectedValue.date.fmt == actualValue.date.fmt && + expectedValue.time.offset == actualValue.time.offset && + expectedValue.date.j.day == actualValue.date.j.day); + case time_zone_posix::PosixTransition::DateFormat::N: + return (expectedValue.date.fmt == actualValue.date.fmt && + expectedValue.time.offset == actualValue.time.offset && + expectedValue.date.n.day == actualValue.date.n.day); + case time_zone_posix::PosixTransition::DateFormat::M: + return (expectedValue.date.fmt == actualValue.date.fmt && + expectedValue.time.offset == actualValue.time.offset && + expectedValue.date.m.month == actualValue.date.m.month && + expectedValue.date.m.week == actualValue.date.m.week && + expectedValue.date.m.weekday == actualValue.date.m.weekday); + } + } + + void posix_transition_describe_value( + const char *label, time_zone_posix::PosixTransition value, + __llvm_libc::testutils::StreamWrapper &stream) { + switch (value.date.fmt) { + case time_zone_posix::PosixTransition::DateFormat::J: + stream << label << ".date.fmt: J "; + stream << label << ".date.j.day: " << value.date.j.day; + break; + case time_zone_posix::PosixTransition::DateFormat::N: + stream << label << ".date.fmt: N "; + stream << label << ".date.n.day: " << value.date.n.day; + break; + case time_zone_posix::PosixTransition::DateFormat::M: + stream << label << ".date.fmt: M "; + stream << label + << ".date.m.month: " << static_cast(value.date.m.month); + stream << label + << ".date.m.week: " << static_cast(value.date.m.week); + stream << label + << ".date.m.weekday: " << static_cast(value.date.m.weekday); + break; + } + stream << label << ".time.offset: " << value.time.offset; + } + + bool match(time_zone_posix::PosixTimeZone actualValue) { + actual = actualValue; + return (actual.spec == expected.spec && + actual.std_abbr == expected.std_abbr && + actual.std_offset == expected.std_offset && + actual.dst_abbr == expected.dst_abbr && + actual.dst_offset == expected.dst_offset && + posix_transition_equals(expected.dst_start, actual.dst_start) && + posix_transition_equals(expected.dst_end, actual.dst_end)); + } + + void describeValue(const char *label, time_zone_posix::PosixTimeZone value, + __llvm_libc::testutils::StreamWrapper &stream) { + stream << label; + if (value.spec.data()) + stream << " m_spec: " << value.spec.data(); + else + stream << " m_spec: null"; + stream << " m_std_abbr: " << value.std_abbr.data(); + stream << " m_std_offset: " << value.std_offset; + stream << " m_dst_abbr: " << value.dst_abbr.data(); + stream << " m_dst_offset: " << value.dst_offset; + posix_transition_describe_value(" m_dst_start", value.dst_start, stream); + posix_transition_describe_value(" m_dst_end", value.dst_end, stream); + stream << '\n'; + } + + void explainError(__llvm_libc::testutils::StreamWrapper &stream) override { + describeValue("Expected PosixTimeZone value: ", expected, stream); + describeValue(" Actual PosixTimeZone value: ", actual, stream); + } +}; + +} // namespace testing +} // namespace tzmatcher +} // namespace __llvm_libc + +#define EXPECT_POSIX_TIME_ZONE_EQ(expected, actual) \ + EXPECT_THAT((actual), __llvm_libc::tzmatcher::testing::PosixTimeZoneMatcher( \ + (expected))) + +#endif // LLVM_LIBC_TEST_SRC_TIME_TZ_MATCHER_H diff --git a/libc/test/src/time/mktime_test.cpp b/libc/test/src/time/mktime_test.cpp --- a/libc/test/src/time/mktime_test.cpp +++ b/libc/test/src/time/mktime_test.cpp @@ -19,413 +19,418 @@ using __llvm_libc::testing::ErrnoSetterMatcher::Fails; using __llvm_libc::testing::ErrnoSetterMatcher::Succeeds; -using __llvm_libc::time_utils::TimeConstants; +using __llvm_libc::time_utils::Month; -static inline time_t call_mktime(struct tm *tm_data, int year, int month, - int mday, int hour, int min, int sec, int wday, - int yday) { - __llvm_libc::tmhelper::testing::initialize_tm_data( - tm_data, year, month, mday, hour, min, sec, wday, yday); - return __llvm_libc::mktime(tm_data); +static inline constexpr int tm_year(int year) { + return year - TimeConstants::TIME_YEAR_BASE; } TEST(LlvmLibcMkTime, FailureSetsErrno) { - struct tm tm_data; - __llvm_libc::tmhelper::testing::initialize_tm_data( - &tm_data, INT_MAX, INT_MAX, INT_MAX, INT_MAX, INT_MAX, -1, 0, 0); + struct tm tm_data { + .tm_sec = INT_MAX, .tm_min = INT_MAX, .tm_hour = INT_MAX, + .tm_mday = INT_MAX, .tm_mon = INT_MAX - 1, .tm_year = tm_year(INT_MAX), + .tm_wday = 0, .tm_yday = 0 + }; EXPECT_THAT(__llvm_libc::mktime(&tm_data), Fails(EOVERFLOW)); } TEST(LlvmLibcMkTime, InvalidSeconds) { - struct tm tm_data; - // -1 second from 1970-01-01 00:00:00 returns 1969-12-31 23:59:59. - EXPECT_THAT(call_mktime(&tm_data, - 1970, // year - 1, // month - 1, // day - 0, // hr - 0, // min - -1, // sec - 0, // wday - 0), // yday - Succeeds(-1)); - EXPECT_TM_EQ((tm{59, // sec - 59, // min - 23, // hr - 31, // day - 12 - 1, // tm_mon starts with 0 for Jan - 1969 - TimeConstants::TIME_YEAR_BASE, // year - 3, // wday - 364, // yday - 0}), - tm_data); - // 60 seconds from 1970-01-01 00:00:00 returns 1970-01-01 00:01:00. - EXPECT_THAT(call_mktime(&tm_data, - 1970, // year - 1, // month - 1, // day - 0, // hr - 0, // min - 60, // sec - 0, // wday - 0), // yday - Succeeds(60)); - EXPECT_TM_EQ((tm{0, // sec - 1, // min - 0, // hr - 1, // day - 0, // tm_mon starts with 0 for Jan - 1970 - TimeConstants::TIME_YEAR_BASE, // year - 4, // wday - 0, // yday - 0}), - tm_data); + { + // -1 second from 1970-01-01 00:00:00 returns 1969-12-31 23:59:59. + struct tm tm_data { + .tm_sec = -1, .tm_min = 0, .tm_hour = 0, .tm_mday = 1, + .tm_mon = Month::JANUARY, .tm_year = tm_year(1970), .tm_wday = 0, + .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), Succeeds(-1)); + EXPECT_TM_EQ((tm{.tm_sec = 59, + .tm_min = 59, + .tm_hour = 23, + .tm_mday = 31, + .tm_mon = Month::DECEMBER, + .tm_year = tm_year(1969), + .tm_wday = 3, + .tm_yday = 364}), + tm_data); + } + + { + // 60 seconds from 1970-01-01 00:00:00 returns 1970-01-01 00:01:00. + struct tm tm_data { + .tm_sec = 60, .tm_min = 0, .tm_hour = 0, .tm_mday = 1, + .tm_mon = Month::JANUARY, .tm_year = tm_year(1970), .tm_wday = 0, + .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), Succeeds(60)); + EXPECT_TM_EQ((tm{.tm_sec = 0, + .tm_min = 1, + .tm_hour = 0, + .tm_mday = 1, + .tm_mon = Month::JANUARY, + .tm_year = tm_year(1970), + .tm_wday = 4, + .tm_yday = 0}), + tm_data); + } } TEST(LlvmLibcMkTime, InvalidMinutes) { - struct tm tm_data; - // -1 minute from 1970-01-01 00:00:00 returns 1969-12-31 23:59:00. - EXPECT_THAT(call_mktime(&tm_data, - 1970, // year - 1, // month - 1, // day - 0, // hr - -1, // min - 0, // sec - 0, // wday - 0), // yday - Succeeds(-TimeConstants::SECONDS_PER_MIN)); - EXPECT_TM_EQ((tm{0, // sec - 59, // min - 23, // hr - 31, // day - 11, // tm_mon starts with 0 for Jan - 1969 - TimeConstants::TIME_YEAR_BASE, // year - 3, // wday - 0, // yday - 0}), - tm_data); - // 60 minutes from 1970-01-01 00:00:00 returns 1970-01-01 01:00:00. - EXPECT_THAT(call_mktime(&tm_data, - 1970, // year - 1, // month - 1, // day - 0, // hr - 60, // min - 0, // sec - 0, // wday - 0), // yday - Succeeds(60 * TimeConstants::SECONDS_PER_MIN)); - EXPECT_TM_EQ((tm{0, // sec - 0, // min - 1, // hr - 1, // day - 0, // tm_mon starts with 0 for Jan - 1970 - TimeConstants::TIME_YEAR_BASE, // year - 4, // wday - 0, // yday - 0}), - tm_data); + { + // -1 minute from 1970-01-01 00:00:00 returns 1969-12-31 23:59:00. + struct tm tm_data { + .tm_sec = 0, .tm_min = -1, .tm_hour = 0, .tm_mday = 1, + .tm_mon = Month::JANUARY, .tm_year = tm_year(1970), .tm_wday = 0, + .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), + Succeeds(-TimeConstants::SECONDS_PER_MIN)); + EXPECT_TM_EQ((tm{.tm_sec = 0, + .tm_min = 59, + .tm_hour = 23, + .tm_mday = 31, + .tm_mon = Month::DECEMBER, + .tm_year = tm_year(1969), + .tm_wday = 3, + .tm_yday = 0}), + tm_data); + } + + { + // 60 minutes from 1970-01-01 00:00:00 returns 1970-01-01 01:00:00. + struct tm tm_data { + .tm_sec = 0, .tm_min = 60, .tm_hour = 0, .tm_mday = 1, + .tm_mon = Month::JANUARY, .tm_year = tm_year(1970), .tm_wday = 0, + .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), + Succeeds(60 * TimeConstants::SECONDS_PER_MIN)); + EXPECT_TM_EQ((tm{.tm_sec = 0, + .tm_min = 0, + .tm_hour = 1, + .tm_mday = 1, + .tm_mon = Month::JANUARY, + .tm_year = tm_year(1970), + .tm_wday = 4, + .tm_yday = 0}), + tm_data); + } } TEST(LlvmLibcMkTime, InvalidHours) { - struct tm tm_data; - // -1 hour from 1970-01-01 00:00:00 returns 1969-12-31 23:00:00. - EXPECT_THAT(call_mktime(&tm_data, - 1970, // year - 1, // month - 1, // day - -1, // hr - 0, // min - 0, // sec - 0, // wday - 0), // yday - Succeeds(-TimeConstants::SECONDS_PER_HOUR)); - EXPECT_TM_EQ((tm{0, // sec - 0, // min - 23, // hr - 31, // day - 11, // tm_mon starts with 0 for Jan - 1969 - TimeConstants::TIME_YEAR_BASE, // year - 3, // wday - 0, // yday - 0}), - tm_data); - // 24 hours from 1970-01-01 00:00:00 returns 1970-01-02 00:00:00. - EXPECT_THAT(call_mktime(&tm_data, - 1970, // year - 1, // month - 1, // day - 24, // hr - 0, // min - 0, // sec - 0, // wday - 0), // yday - Succeeds(24 * TimeConstants::SECONDS_PER_HOUR)); - EXPECT_TM_EQ((tm{0, // sec - 0, // min - 0, // hr - 2, // day - 0, // tm_mon starts with 0 for Jan - 1970 - TimeConstants::TIME_YEAR_BASE, // year - 5, // wday - 0, // yday - 0}), - tm_data); + { + // -1 hour from 1970-01-01 00:00:00 returns 1969-12-31 23:00:00. + struct tm tm_data { + .tm_sec = 0, .tm_min = 0, .tm_hour = -1, .tm_mday = 1, + .tm_mon = Month::JANUARY, .tm_year = tm_year(1970), .tm_wday = 0, + .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), + Succeeds(-TimeConstants::SECONDS_PER_HOUR)); + EXPECT_TM_EQ((tm{.tm_sec = 0, + .tm_min = 0, + .tm_hour = 23, + .tm_mday = 31, + .tm_mon = Month::DECEMBER, + .tm_year = tm_year(1969), + .tm_wday = 3, + .tm_yday = 0}), + tm_data); + } + + { + // 24 hours from 1970-01-01 00:00:00 returns 1970-01-02 00:00:00. + struct tm tm_data { + .tm_sec = 0, .tm_min = 0, .tm_hour = 24, .tm_mday = 1, + .tm_mon = Month::JANUARY, .tm_year = tm_year(1970), .tm_wday = 0, + .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), + Succeeds(24 * TimeConstants::SECONDS_PER_HOUR)); + EXPECT_TM_EQ((tm{.tm_sec = 0, + .tm_min = 0, + .tm_hour = 0, + .tm_mday = 2, + .tm_mon = Month::JANUARY, + .tm_year = tm_year(1970), + .tm_wday = 5, + .tm_yday = 0}), + tm_data); + } } TEST(LlvmLibcMkTime, InvalidYear) { - struct tm tm_data; // -1 year from 1970-01-01 00:00:00 returns 1969-01-01 00:00:00. - EXPECT_THAT(call_mktime(&tm_data, - 1969, // year - 1, // month - 1, // day - 0, // hr - 0, // min - 0, // sec - 0, // wday - 0), // yday + struct tm tm_data { + .tm_sec = 0, .tm_min = 0, .tm_hour = 0, .tm_mday = 1, + .tm_mon = Month::JANUARY, .tm_year = tm_year(1969), .tm_wday = 0, + .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), Succeeds(-TimeConstants::DAYS_PER_NON_LEAP_YEAR * TimeConstants::SECONDS_PER_DAY)); - EXPECT_TM_EQ((tm{0, // sec - 0, // min - 0, // hr - 1, // day - 0, // tm_mon starts with 0 for Jan - 1969 - TimeConstants::TIME_YEAR_BASE, // year - 3, // wday - 0, // yday - 0}), + EXPECT_TM_EQ((tm{.tm_sec = 0, + .tm_min = 0, + .tm_hour = 0, + .tm_mday = 1, + .tm_mon = Month::JANUARY, + .tm_year = tm_year(1969), + .tm_wday = 3, + .tm_yday = 0}), tm_data); } TEST(LlvmLibcMkTime, InvalidEndOf32BitEpochYear) { if (sizeof(size_t) != 4) return; - struct tm tm_data; - // 2038-01-19 03:14:08 tests overflow of the second in 2038. - EXPECT_THAT(call_mktime(&tm_data, 2038, 1, 19, 3, 14, 8, 0, 0), - Succeeds(TimeConstants::OUT_OF_RANGE_RETURN_VALUE)); - // 2038-01-19 03:15:07 tests overflow of the minute in 2038. - EXPECT_THAT(call_mktime(&tm_data, 2038, 1, 19, 3, 15, 7, 0, 0), - Succeeds(TimeConstants::OUT_OF_RANGE_RETURN_VALUE)); - // 2038-01-19 04:14:07 tests overflow of the hour in 2038. - EXPECT_THAT(call_mktime(&tm_data, 2038, 1, 19, 4, 14, 7, 0, 0), - Succeeds(TimeConstants::OUT_OF_RANGE_RETURN_VALUE)); - // 2038-01-20 03:14:07 tests overflow of the day in 2038. - EXPECT_THAT(call_mktime(&tm_data, 2038, 1, 20, 3, 14, 7, 0, 0), - Succeeds(TimeConstants::OUT_OF_RANGE_RETURN_VALUE)); - // 2038-02-19 03:14:07 tests overflow of the month in 2038. - EXPECT_THAT(call_mktime(&tm_data, 2038, 2, 19, 3, 14, 7, 0, 0), - Succeeds(TimeConstants::OUT_OF_RANGE_RETURN_VALUE)); - // 2039-01-19 03:14:07 tests overflow of the year. - EXPECT_THAT(call_mktime(&tm_data, 2039, 1, 19, 3, 14, 7, 0, 0), - Succeeds(TimeConstants::OUT_OF_RANGE_RETURN_VALUE)); + { + // 2038-01-19 03:14:08 tests overflow of the second in 2038. + struct tm tm_data { + .tm_sec = 8, .tm_min = 14, .tm_hour = 3, .tm_mday = 19, + .tm_mon = Month::JANUARY, .tm_year = tm_year(2038), .tm_wday = 0, + .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), + Succeeds(TimeConstants::OUT_OF_RANGE_RETURN_VALUE)); + } + + { + // 2038-01-19 03:15:07 tests overflow of the minute in 2038. + struct tm tm_data { + .tm_sec = 7, .tm_min = 15, .tm_hour = 3, .tm_mday = 19, + .tm_mon = Month::JANUARY, .tm_year = tm_year(2038), .tm_wday = 0, + .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), + Succeeds(TimeConstants::OUT_OF_RANGE_RETURN_VALUE)); + } + + { + // 2038-01-19 04:14:07 tests overflow of the hour in 2038. + struct tm tm_data { + .tm_sec = 7, .tm_min = 14, .tm_hour = 4, .tm_mday = 19, + .tm_mon = Month::JANUARY, .tm_year = tm_year(2038), .tm_wday = 0, + .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), + Succeeds(TimeConstants::OUT_OF_RANGE_RETURN_VALUE)); + } + + { + // 2038-01-20 03:14:07 tests overflow of the day in 2038. + struct tm tm_data { + .tm_sec = 7, .tm_min = 14, .tm_hour = 3, .tm_mday = 20, + .tm_mon = Month::JANUARY, .tm_year = tm_year(2038), .tm_wday = 0, + .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), + Succeeds(TimeConstants::OUT_OF_RANGE_RETURN_VALUE)); + } + + { + // 2038-02-19 03:14:07 tests overflow of the month in 2038. + struct tm tm_data { + .tm_sec = 7, .tm_min = 14, .tm_hour = 3, .tm_mday = 19, + .tm_mon = Month::FEBRUARY, .tm_year = tm_year(2038), .tm_wday = 0, + .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), + Succeeds(TimeConstants::OUT_OF_RANGE_RETURN_VALUE)); + } + + { + // 2039-01-19 03:14:07 tests overflow of the year. + struct tm tm_data { + .tm_sec = 7, .tm_min = 14, .tm_hour = 3, .tm_mday = 19, + .tm_mon = Month::JANUARY, .tm_year = tm_year(2039), .tm_wday = 0, + .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), + Succeeds(TimeConstants::OUT_OF_RANGE_RETURN_VALUE)); + } } TEST(LlvmLibcMkTime, InvalidMonths) { - struct tm tm_data; - // -1 month from 1970-01-01 00:00:00 returns 1969-12-01 00:00:00. - EXPECT_THAT(call_mktime(&tm_data, - 1970, // year - 0, // month - 1, // day - 0, // hr - 0, // min - 0, // sec - 0, // wday - 0), // yday - Succeeds(-31 * TimeConstants::SECONDS_PER_DAY)); - EXPECT_TM_EQ((tm{0, // sec - 0, // min - 0, // hr - 1, // day - 12 - 1, // tm_mon starts with 0 for Jan - 1969 - TimeConstants::TIME_YEAR_BASE, // year - 1, // wday - 0, // yday - 0}), - tm_data); - // 1970-13-01 00:00:00 returns 1971-01-01 00:00:00. - EXPECT_THAT(call_mktime(&tm_data, - 1970, // year - 13, // month - 1, // day - 0, // hr - 0, // min - 0, // sec - 0, // wday - 0), // yday - Succeeds(TimeConstants::DAYS_PER_NON_LEAP_YEAR * - TimeConstants::SECONDS_PER_DAY)); - EXPECT_TM_EQ((tm{0, // sec - 0, // min - 0, // hr - 1, // day - 0, // tm_mon starts with 0 for Jan - 1971 - TimeConstants::TIME_YEAR_BASE, // year - 5, // wday - 0, // yday - 0}), - tm_data); + { + // -1 month from 1970-01-01 00:00:00 returns 1969-12-01 00:00:00. + struct tm tm_data { + .tm_sec = 0, .tm_min = 0, .tm_hour = 0, .tm_mday = 0, .tm_mon = -1, + .tm_year = tm_year(1970), .tm_wday = 0, .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), + Succeeds(-32 * TimeConstants::SECONDS_PER_DAY)); + EXPECT_TM_EQ((tm{.tm_sec = 0, + .tm_min = 0, + .tm_hour = 0, + .tm_mday = 1, + .tm_mon = Month::DECEMBER, + .tm_year = tm_year(1969), + .tm_wday = 1, + .tm_yday = 0}), + tm_data); + } + + { + // 1970-13-01 00:00:00 returns 1971-01-01 00:00:00. + struct tm tm_data { + .tm_sec = 0, .tm_min = 0, .tm_hour = 0, .tm_mday = 1, .tm_mon = 12, + .tm_year = tm_year(1970), .tm_wday = 0, .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), + Succeeds(TimeConstants::DAYS_PER_NON_LEAP_YEAR * + TimeConstants::SECONDS_PER_DAY)); + EXPECT_TM_EQ((tm{.tm_sec = 0, + .tm_min = 0, + .tm_hour = 0, + .tm_mday = 1, + .tm_mon = Month::JANUARY, + .tm_year = tm_year(1971), + .tm_wday = 5, + .tm_yday = 0}), + tm_data); + } } TEST(LlvmLibcMkTime, InvalidDays) { - struct tm tm_data; - // -1 day from 1970-01-01 00:00:00 returns 1969-12-31 00:00:00. - EXPECT_THAT(call_mktime(&tm_data, - 1970, // year - 1, // month - 0, // day - 0, // hr - 0, // min - 0, // sec - 0, // wday - 0), // yday - Succeeds(-1 * TimeConstants::SECONDS_PER_DAY)); - EXPECT_TM_EQ((tm{0, // sec - 0, // min - 0, // hr - 31, // day - 11, // tm_mon starts with 0 for Jan - 1969 - TimeConstants::TIME_YEAR_BASE, // year - 3, // wday - 0, // yday - 0}), - tm_data); + { + // -1 day from 1970-01-01 00:00:00 returns 1969-12-31 00:00:00. + struct tm tm_data { + .tm_sec = 0, .tm_min = 0, .tm_hour = 0, .tm_mday = (1 - 1), + .tm_mon = Month::JANUARY, .tm_year = tm_year(1970), .tm_wday = 0, + .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), + Succeeds(-1 * TimeConstants::SECONDS_PER_DAY)); + EXPECT_TM_EQ((tm{.tm_sec = 0, + .tm_min = 0, + .tm_hour = 0, + .tm_mday = 31, + .tm_mon = Month::DECEMBER, + .tm_year = tm_year(1969), + .tm_wday = 3, + .tm_yday = 0}), + tm_data); + } - // 1970-01-32 00:00:00 returns 1970-02-01 00:00:00. - EXPECT_THAT(call_mktime(&tm_data, - 1970, // year - 1, // month - 32, // day - 0, // hr - 0, // min - 0, // sec - 0, // wday - 0), // yday - Succeeds(31 * TimeConstants::SECONDS_PER_DAY)); - EXPECT_TM_EQ((tm{0, // sec - 0, // min - 0, // hr - 1, // day - 0, // tm_mon starts with 0 for Jan - 1970 - TimeConstants::TIME_YEAR_BASE, // year - 0, // wday - 0, // yday - 0}), - tm_data); + { + // 1970-01-32 00:00:00 returns 1970-02-01 00:00:00. + struct tm tm_data { + .tm_sec = 0, .tm_min = 0, .tm_hour = 0, .tm_mday = 32, + .tm_mon = Month::JANUARY, .tm_year = tm_year(1970), .tm_wday = 0, + .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), + Succeeds(31 * TimeConstants::SECONDS_PER_DAY)); + EXPECT_TM_EQ((tm{.tm_sec = 0, + .tm_min = 0, + .tm_hour = 0, + .tm_mday = 1, + .tm_mon = Month::FEBRUARY, + .tm_year = tm_year(1970), + .tm_wday = 0, + .tm_yday = 0}), + tm_data); + } - // 1970-02-29 00:00:00 returns 1970-03-01 00:00:00. - EXPECT_THAT(call_mktime(&tm_data, - 1970, // year - 2, // month - 29, // day - 0, // hr - 0, // min - 0, // sec - 0, // wday - 0), // yday - Succeeds(59 * TimeConstants::SECONDS_PER_DAY)); - EXPECT_TM_EQ((tm{0, // sec - 0, // min - 0, // hr - 1, // day - 2, // tm_mon starts with 0 for Jan - 1970 - TimeConstants::TIME_YEAR_BASE, // year - 0, // wday - 0, // yday - 0}), - tm_data); + { + // 1970-02-29 00:00:00 returns 1970-03-01 00:00:00. + struct tm tm_data { + .tm_sec = 0, .tm_min = 0, .tm_hour = 0, .tm_mday = 29, + .tm_mon = Month::FEBRUARY, .tm_year = tm_year(1970), .tm_wday = 0, + .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), + Succeeds(59 * TimeConstants::SECONDS_PER_DAY)); + EXPECT_TM_EQ((tm{.tm_sec = 0, + .tm_min = 0, + .tm_hour = 0, + .tm_mday = 1, + .tm_mon = Month::MARCH, + .tm_year = tm_year(1970), + .tm_wday = 0, + .tm_yday = 0}), + tm_data); + } - // 1972-02-30 00:00:00 returns 1972-03-01 00:00:00. - EXPECT_THAT(call_mktime(&tm_data, - 1972, // year - 2, // month - 30, // day - 0, // hr - 0, // min - 0, // sec - 0, // wday - 0), // yday - Succeeds(((2 * TimeConstants::DAYS_PER_NON_LEAP_YEAR) + 60) * - TimeConstants::SECONDS_PER_DAY)); - EXPECT_TM_EQ((tm{0, // sec - 0, // min - 0, // hr - 1, // day - 2, // tm_mon starts with 0 for Jan - 1972 - TimeConstants::TIME_YEAR_BASE, // year - 3, // wday - 0, // yday - 0}), - tm_data); + { + // 1972-02-30 00:00:00 returns 1972-03-01 00:00:00. + struct tm tm_data { + .tm_sec = 0, .tm_min = 0, .tm_hour = 0, .tm_mday = 30, + .tm_mon = Month::FEBRUARY, .tm_year = tm_year(1972), .tm_wday = 0, + .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), + Succeeds(((2 * TimeConstants::DAYS_PER_NON_LEAP_YEAR) + 60) * + TimeConstants::SECONDS_PER_DAY)); + EXPECT_TM_EQ((tm{.tm_sec = 0, + .tm_min = 0, + .tm_hour = 0, + .tm_mday = 1, + .tm_mon = Month::MARCH, + .tm_year = tm_year(1972), + .tm_wday = 3, + .tm_yday = 0}), + tm_data); + } } TEST(LlvmLibcMkTime, EndOf32BitEpochYear) { - struct tm tm_data; // Test for maximum value of a signed 32-bit integer. // Test implementation can encode time for Tue 19 January 2038 03:14:07 UTC. - EXPECT_THAT(call_mktime(&tm_data, - 2038, // year - 1, // month - 19, // day - 3, // hr - 14, // min - 7, // sec - 0, // wday - 0), // yday - Succeeds(0x7FFFFFFF)); - EXPECT_TM_EQ((tm{7, // sec - 14, // min - 3, // hr - 19, // day - 0, // tm_mon starts with 0 for Jan - 2038 - TimeConstants::TIME_YEAR_BASE, // year - 2, // wday - 7, // yday - 0}), + struct tm tm_data { + .tm_sec = 7, .tm_min = 14, .tm_hour = 3, .tm_mday = 19, + .tm_mon = Month::JANUARY, .tm_year = tm_year(2038), .tm_wday = 0, + .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), Succeeds(0x7FFFFFFF)); + EXPECT_TM_EQ((tm{.tm_sec = 7, + .tm_min = 14, + .tm_hour = 3, + .tm_mday = 19, + .tm_mon = Month::JANUARY, + .tm_year = tm_year(2038), + .tm_wday = 2, + .tm_yday = 7}), tm_data); } TEST(LlvmLibcMkTime, Max64BitYear) { if (sizeof(time_t) == 4) return; - // Mon Jan 1 12:50:50 2170 (200 years from 1970), - struct tm tm_data; - EXPECT_THAT(call_mktime(&tm_data, - 2170, // year - 1, // month - 1, // day - 12, // hr - 50, // min - 50, // sec - 0, // wday - 0), // yday - Succeeds(6311479850)); - EXPECT_TM_EQ((tm{50, // sec - 50, // min - 12, // hr - 1, // day - 0, // tm_mon starts with 0 for Jan - 2170 - TimeConstants::TIME_YEAR_BASE, // year - 1, // wday - 50, // yday - 0}), - tm_data); + { + // Mon Jan 1 12:50:50 2170 (200 years from 1970), + struct tm tm_data { + .tm_sec = 50, .tm_min = 50, .tm_hour = 12, .tm_mday = 1, + .tm_mon = Month::JANUARY, .tm_year = tm_year(2170), .tm_wday = 0, + .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), Succeeds(6311479850)); + EXPECT_TM_EQ((tm{.tm_sec = 50, + .tm_min = 50, + .tm_hour = 12, + .tm_mday = 1, + .tm_mon = Month::JANUARY, + .tm_year = tm_year(2170), + .tm_wday = 1, + .tm_yday = 50}), + tm_data); + } - // Test for Tue Jan 1 12:50:50 in 2,147,483,647th year. - EXPECT_THAT(call_mktime(&tm_data, 2147483647, 1, 1, 12, 50, 50, 0, 0), - Succeeds(67767976202043050)); - EXPECT_TM_EQ((tm{50, // sec - 50, // min - 12, // hr - 1, // day - 0, // tm_mon starts with 0 for Jan - 2147483647 - TimeConstants::TIME_YEAR_BASE, // year - 2, // wday - 50, // yday - 0}), - tm_data); + { + // Test for Tue Jan 1 12:50:50 in 2,147,483,647th year. + struct tm tm_data { + .tm_sec = 50, .tm_min = 50, .tm_hour = 12, .tm_mday = 1, + .tm_mon = Month::JANUARY, .tm_year = tm_year(2147483647), .tm_wday = 0, + .tm_yday = 0 + }; + EXPECT_THAT(__llvm_libc::mktime(&tm_data), Succeeds(67767976202043050)); + EXPECT_TM_EQ((tm{.tm_sec = 50, + .tm_min = 50, + .tm_hour = 12, + .tm_mday = 1, + .tm_mon = Month::JANUARY, + .tm_year = tm_year(2147483647), + .tm_wday = 2, + .tm_yday = 50}), + tm_data); + } } diff --git a/libc/test/src/time/time_zone_posix_test.cpp b/libc/test/src/time/time_zone_posix_test.cpp new file mode 100644 --- /dev/null +++ b/libc/test/src/time/time_zone_posix_test.cpp @@ -0,0 +1,279 @@ +//===-- Unittests for asctime ---------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "src/__support/CPP/string_view.h" +#include "src/time/time_zone_posix.h" +#include "test/ErrnoSetterMatcher.h" +#include "test/src/time/TZMatcher.h" +#include "utils/UnitTest/Test.h" + +#include +#include + +TEST(LlvmLibcParsePosixSpec, InvalidTest) { + const char *bad_timezones[] = { + "", + ":", + " ", + // + // Test for [+|-]hh[:mm[:ss]] in STD and DST offsets. + // + // Test missing hours in STD offset. + "EST", + "EST+", + "EST-", + // Test for missing minutes in STD offset + "EST5:", + "EST+5:", + "EST-5:", + // Test for missing seconds in STD offset + "EST5:0:", + "EST+5:0:", + "EST-5:0:", + // Test invalid hours data in STD Offset. + "EST25", + "EST+25", + // Test invalid minutes data in STD offset. + "EST5:-1:0", + "EST5:60:0", + "EST+5:-1:0", + "EST+5:60:0", + "EST-5:-1:0", + "EST-5:60:0", + // Test invalid seconds data in STD offset. + "EST5:0:-1", + "EST5:0:60", + "EST+5:0:-1", + "EST+5:0:60", + "EST-5:0:-1", + "EST-5:0:60", + + // Test missing hours in DST offset. + "EST5EDT", + "EST5EDT+", + "EST5EDT-", + // Test for missing minutes in DST offset + "EST5EDT6:", + "EST5EDT+6:", + "EST5EDT-6:", + // Test for missing seconds in DST offset + "EST5EDT6:0:", + "EST5EDT+6:0:", + "EST5EDT-6:0:", + // Test invalid hours data in DST Offset. + "EST5EDT25", + "EST5EDT+25", + // Test invalid minutes data in DST offset. + "EST5EDT6:-1:0", + "EST5EDT6:60:0", + "EST5EDT+6:-1:0", + "EST5EDT+6:60:0", + "EST5EDT-6:-1:0", + "EST5EDT-6:60:0", + // Test invalid seconds data in DST offset. + "EST5EDT6:0:-1", + "EST5EDT6:0:60", + "EST5EDT+6:0:-1", + "EST5EDT+6:0:60", + "EST5EDT-6:0:-1", + "EST5EDT-6:0:60", + + // Test extra unnecessary data at the end of DST offset + // (start_date and end_date or optional) + "EST5EDT6AAA", + "EST5EDT+6AAA", + "EST5EDT-6AAA", + "EST5EDT6:59BBB", + "EST5EDT+6:59BBB", + "EST5EDT-6:59BBB", + "EST5EDT6:59:59CCC", + "EST5EDT+6:59:59CCC", + "EST5EDT-6:59:59CCC", + + // Test invalid time offset in start_date + "PST8PDT,", + "PST8PDT,M3", + "PST8PDT,M3.", + "PST8PDT,M3.2", + "PST8PDT,M3.2.", + "PST8PDT,M3.2.0,", + "PST8PDT,M3.2.0/", + "PST8PDT,M3.2.0/24:", + "PST8PDT,M3.2.0/24:59:", + "PST8PDT,M3.2.0/168", + "PST8PDT,M3.2.0/+168", + "PST8PDT,M3.2.0/-168", + "PST8PDT,M3.2.0/24:-1:59", + "PST8PDT,M3.2.0/24:60:59", + "PST8PDT,M3.2.0/24:0:-1", + "PST8PDT,M3.2.0/24:0:60", + // Test invalid time offset in end_date + "PST8PDT,M3.2.0,M11", + "PST8PDT,M3.2.0,M11.", + "PST8PDT,M3.2.0,M11.1", + "PST8PDT,M3.2.0,M11.1.", + "PST8PDT,M3.2.0,M11.1.0/", + "PST8PDT,M3.2.0,M11.1.0/24:", + "PST8PDT,M3.2.0,M11.1.0/24:59:", + "PST8PDT,M3.2.0,M11.1.0/168", + "PST8PDT,M3.2.0,M11.1.0/+168", + "PST8PDT,M3.2.0,M11.1.0/-168", + "PST8PDT,M3.2.0,M11.1.0/24:-1:59", + "PST8PDT,M3.2.0,M11.1.0/24:60:59", + "PST8PDT,M3.2.0,M11.1.0/24:0:-1", + "PST8PDT,M3.2.0,M11.1.0/24:0:60", + + // Test invalid data in MonthWeekWeekday + "PST8PDT,M0.2.0,M11.1.0", // Test 0 as month in start_date + "PST8PDT,M13.2.0,M11.1.0", // Test 13 as month in start_date + "PST8PDT,M1.0.0,M11.1.0", // Test 0 as week in start_date + "PST8PDT,M1.6.0,M11.1.0", // Test 6 as week in start_date + "PST8PDT,M1.2.-1,M11.1.0", // Test -1 as weekday in start_date + "PST8PDT,M1.2.7,M11.1.0", // Test 7 as weekday in start_date + "PST8PDT,M0.2.0,M0.1.0", // Test 0 as month in end_date + "PST8PDT,M13.2.0,M13.1.0", // Test 13 as month in end_date + "PST8PDT,M1.0.0,M11.0.0", // Test 0 as week in end_date + "PST8PDT,M1.6.0,M11.6.0", // Test 6 as week in end_date + "PST8PDT,M1.2.-1,M11.1.-1", // Test -1 as weekday in end_date + "PST8PDT,M1.2.7,M11.1.7", // Test 7 as weekday in end_date + "PST8PDT,J0", // Test 0 as nonLeapDay in start_date + "PST8PDT,J366", // Test 366 as nonLeapDay in start_date + "PST8PDT,J1,J0", // Test 0 as nonLeapDay in end_date + "PST8PDT,J1,J366", // Test 366 as nonLeapDay in end_date + "PST8PDT,-1", // Test -1 as LeapDay in start_date + "PST8PDT,366", // Test 366 as LeapDay in start_date + "PST8PDT,1,-1", // Test -1 as LeapDay in end_date + "PST8PDT,1,366", // Test 366 as LeapDay in end_date + + // Test extra unnecessary data at the end. + "PST8PDT,M3.2.0,M11.1.0AA", + "PST8PDT,M3.2.0BB", + "PST8PDT,J59CC", + "PST8PDT,J59,J58DD", + "PST8PDT,59EE", + "PST8PDT,59,58FF", + }; + + for (const auto &timezone : bad_timezones) { + __llvm_libc::cpp::string_view timezone_spec(timezone); + const auto posix_result = + __llvm_libc::time_zone_posix::PosixTimeZone::ParsePosixSpec( + timezone_spec); + // TODO(rtenneti): Use a special matcher to verify the data. + if (posix_result.has_value()) { + __builtin_printf( + "Testing failed for: %s - expected to faile but succeed.\n", + timezone); + } + ASSERT_FALSE(posix_result.has_value()); + } +} + +TEST(LlvmLibcParsePosixSpec, ValidTest) { + const char *good_timezones[] = { + "EST5", + "EST5:0", + "EST5:59", + "EST5:0:0", + "EST5:0:59", + "EST+5", + "EST+5:0", + "EST+5:59", + "EST+5:0:0", + "EST+5:0:59", + "EST-5", + "EST-5:0", + "EST-5:59", + "EST-5:0:0", + "EST-5:0:59", + "EST5EDT6", + "EST5EDT6/1:2:3", + "EST5EDT6:0", + "EST5EDT6:0/1:2:3", + "EST5EDT6:59", + "EST5EDT6:59/1:2:3", + "EST5EDT6:0:0", + "EST5EDT6:0:0/1:2:3", + "EST5EDT6:0:59", + "EST5EDT6:0:59/1:2:3", + "EST5EDT+6", + "EST5EDT+6/1:2:3", + "EST5EDT+6:0", + "EST5EDT+6:0/1:2:3", + "EST5EDT+6:59", + "EST5EDT+6:59/1:2:3", + "EST5EDT+6:0:0", + "EST5EDT+6:0:0/1:2:3", + "EST5EDT+6:0:59", + "EST5EDT+6:0:59/1:2:3", + "EST5EDT-6", + "EST5EDT-6/1:2:3", + "EST5EDT-6:0", + "EST5EDT-6:0/1:2:3", + "EST5EDT-6:59", + "EST5EDT-6:59/1:2:3", + "EST5EDT-6:0:0", + "EST5EDT-6:0:0/1:2:3", + "EST5EDT-6:0:59", + "EST5EDT-6:0:59/1:2:3", + "PST8PDT,M3.2.0", + "PST8PDT,M3.2.0/1:2:3", + "PST8PDT,M3.2.0,M11.1.0", + "PST8PDT,M3.2.0,M11.1.0/1:2:3", + "PST8PDT,J59", + "PST8PDT,J59/1:2:3", + "PST8PDT,J59,J58", + "PST8PDT,J59,J58/1:2:3", + "PST8PDT,59", + "PST8PDT,59/1:2:3", + "PST8PDT,59,58", + "PST8PDT,59,58/1:2:3", + "CET-1CEST,M3.5.0/2,M10.5.0/3", + }; + + for (const auto &timezone : good_timezones) { + __llvm_libc::cpp::string_view timezone_spec(timezone); + const auto posix_result = + __llvm_libc::time_zone_posix::PosixTimeZone::ParsePosixSpec( + timezone_spec); + // TODO(rtenneti): Use a special matcher to verify the data. + if (!posix_result.has_value()) { + __builtin_printf( + "Testing failed for: %s - expected to succeed but failed\n", + timezone); + } + } +} + +TEST(LlvmLibcParsePosixSpec, ValidTestAndVerify) { + __llvm_libc::cpp::string_view timezone_spec("PST8PDT,M3.2.0,M11.1.0"); + // TODO(rtenneti): Use a special matcher to verify the data. + __llvm_libc::cpp::optional<__llvm_libc::time_zone_posix::PosixTimeZone> + posix_result = + __llvm_libc::time_zone_posix::PosixTimeZone::ParsePosixSpec( + timezone_spec); + ASSERT_TRUE(posix_result.has_value()); + __llvm_libc::time_zone_posix::PosixTimeZone posix = posix_result.value(); + + __llvm_libc::time_zone_posix::PosixTransition dst_start( + __llvm_libc::time_zone_posix::PosixTransition::DateFormat::M, + static_cast(3), static_cast(2), static_cast(0), + static_cast(7200)); + + __llvm_libc::time_zone_posix::PosixTransition dst_end( + __llvm_libc::time_zone_posix::PosixTransition::DateFormat::M, + static_cast(11), static_cast(1), static_cast(0), + static_cast(7200)); + + __llvm_libc::time_zone_posix::PosixTimeZone expected_posix( + __llvm_libc::cpp::string_view(""), __llvm_libc::cpp::string_view("PST"), + static_cast(-28800), __llvm_libc::cpp::string_view("PDT"), + static_cast(-25200), dst_start, dst_end); + + EXPECT_POSIX_TIME_ZONE_EQ(expected_posix, posix); +}