rexglue-skate3/tests/unit/core/chrono_test.cpp
2026-05-18 22:41:13 +01:00

317 lines
12 KiB
C++

/**
* @file tests/unit/core/chrono_test.cpp
* @brief Unit tests for rex::chrono types (NtSystemClock, WinSystemClock)
* and the date:: calendar API surface used in xboxkrnl_rtl.cpp.
*
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
* All rights reserved.
*
* @license BSD 3-Clause License
* See LICENSE file in the project root for full license text.
*/
#include <chrono>
#include <cstdint>
#include <catch2/catch_test_macros.hpp>
#include <rex/chrono/chrono.h>
using rex::chrono::WinSystemClock;
// =============================================================================
// Known FILETIME constants
// =============================================================================
// All values are 100-nanosecond intervals since 1601-01-01 00:00:00 UTC.
static constexpr uint64_t kFtNtEpoch = 0; // 1601-01-01
static constexpr uint64_t kFtUnixEpoch = 116444736000000000; // 1970-01-01
static constexpr uint64_t kFtY2k = 125911584000000000; // 2000-01-01
static constexpr uint64_t kFtLeapDay = 125962560000000000; // 2000-02-29
static constexpr uint64_t kFtSubDay = 132538032123450000; // 2020-12-30 12:00:12.345
static constexpr uint64_t kFt2021 = 132539328000000000; // 2021-01-01
// =============================================================================
// Section 1: Epoch Constant Validation
// =============================================================================
TEST_CASE("unix_epoch_delta computes correct 1601-to-1970 offset", "[chrono]") {
// 369 years from 1601 to 1970, with 89 leap days
constexpr int64_t expected_seconds = (369LL * 365 + 89) * 86400LL;
// unix_epoch_delta() computes dynamically from date types
// The delta should be negative (1601 is before 1970)
CHECK(WinSystemClock::unix_epoch_delta().count() == -expected_seconds);
CHECK(WinSystemClock::unix_epoch_delta().count() < 0);
}
// =============================================================================
// Section 2: FILETIME Round-Trip
// =============================================================================
TEST_CASE("from_file_time and to_file_time are exact inverses", "[chrono]") {
SECTION("zero (NT epoch)") {
auto tp = WinSystemClock::from_file_time(kFtNtEpoch);
CHECK(WinSystemClock::to_file_time(tp) == kFtNtEpoch);
}
SECTION("Unix epoch") {
auto tp = WinSystemClock::from_file_time(kFtUnixEpoch);
CHECK(WinSystemClock::to_file_time(tp) == kFtUnixEpoch);
}
SECTION("large value") {
constexpr uint64_t large = 2650467743990000000ULL; // ~year 9999
auto tp = WinSystemClock::from_file_time(large);
CHECK(WinSystemClock::to_file_time(tp) == large);
}
}
TEST_CASE("to_file_time then from_file_time round-trips", "[chrono]") {
auto tp = WinSystemClock::from_file_time(kFtSubDay);
auto ft = WinSystemClock::to_file_time(tp);
auto tp2 = WinSystemClock::from_file_time(ft);
CHECK(tp == tp2);
}
// =============================================================================
// Section 3: to_sys / from_sys with Known Values
// =============================================================================
TEST_CASE("to_sys converts known FILETIMEs to correct system_clock values", "[chrono]") {
using namespace std::chrono;
SECTION("Unix epoch FILETIME -> system_clock epoch") {
auto tp = WinSystemClock::to_sys(WinSystemClock::from_file_time(kFtUnixEpoch));
// system_clock epoch is 1970-01-01, so time_since_epoch should be zero
auto since_epoch = tp.time_since_epoch();
CHECK(duration_cast<seconds>(since_epoch).count() == 0);
}
SECTION("Y2K FILETIME -> 30 years after Unix epoch") {
auto tp = WinSystemClock::to_sys(WinSystemClock::from_file_time(kFtY2k));
auto since_epoch = tp.time_since_epoch();
// 2000-01-01 is 10957 days after 1970-01-01 (including leap days)
CHECK(duration_cast<seconds>(since_epoch).count() == 10957LL * 86400);
}
SECTION("2021 FILETIME -> known seconds since Unix epoch") {
auto tp = WinSystemClock::to_sys(WinSystemClock::from_file_time(kFt2021));
auto since_epoch = tp.time_since_epoch();
// 2021-01-01 is 18628 days after 1970-01-01
CHECK(duration_cast<seconds>(since_epoch).count() == 18628LL * 86400);
}
}
TEST_CASE("from_sys then to_sys round-trips for whole-second values", "[chrono]") {
// Use values aligned to system_clock precision (whole seconds)
for (uint64_t ft : {kFtNtEpoch, kFtUnixEpoch, kFtY2k, kFt2021}) {
auto nt_tp = WinSystemClock::from_file_time(ft);
auto sys_tp = WinSystemClock::to_sys(nt_tp);
auto nt_tp2 = WinSystemClock::from_sys(sys_tp);
auto ft2 = WinSystemClock::to_file_time(nt_tp2);
CHECK(ft2 == ft);
}
}
// =============================================================================
// Section 4: Calendar Decomposition (mirrors RtlTimeToTimeFields)
// =============================================================================
// Helper struct matching the fields extracted in RtlTimeToTimeFields_entry
struct TimeFields {
int year;
unsigned month;
unsigned day;
unsigned weekday; // c_encoding: 0=Sun..6=Sat
int hours;
int minutes;
int seconds;
int milliseconds;
};
// Decompose a FILETIME exactly as RtlTimeToTimeFields_entry does
static TimeFields decompose(uint64_t filetime) {
auto tp = WinSystemClock::to_sys(WinSystemClock::from_file_time(filetime));
auto dp = std::chrono::floor<std::chrono::days>(tp);
auto ymd = std::chrono::year_month_day{dp};
auto wd = std::chrono::weekday{dp};
auto time = std::chrono::hh_mm_ss{std::chrono::floor<std::chrono::milliseconds>(tp - dp)};
return {
static_cast<int>(ymd.year()),
static_cast<unsigned>(ymd.month()),
static_cast<unsigned>(ymd.day()),
wd.c_encoding(),
static_cast<int>(time.hours().count()),
static_cast<int>(time.minutes().count()),
static_cast<int>(time.seconds().count()),
static_cast<int>(time.subseconds().count()),
};
}
TEST_CASE("calendar decomposition: NT epoch (1601-01-01)", "[chrono]") {
auto tf = decompose(kFtNtEpoch);
CHECK(tf.year == 1601);
CHECK(tf.month == 1);
CHECK(tf.day == 1);
CHECK(tf.weekday == 1); // Monday
CHECK(tf.hours == 0);
CHECK(tf.minutes == 0);
CHECK(tf.seconds == 0);
CHECK(tf.milliseconds == 0);
}
TEST_CASE("calendar decomposition: Unix epoch (1970-01-01)", "[chrono]") {
auto tf = decompose(kFtUnixEpoch);
CHECK(tf.year == 1970);
CHECK(tf.month == 1);
CHECK(tf.day == 1);
CHECK(tf.weekday == 4); // Thursday
CHECK(tf.hours == 0);
CHECK(tf.minutes == 0);
CHECK(tf.seconds == 0);
CHECK(tf.milliseconds == 0);
}
TEST_CASE("calendar decomposition: Y2K (2000-01-01)", "[chrono]") {
auto tf = decompose(kFtY2k);
CHECK(tf.year == 2000);
CHECK(tf.month == 1);
CHECK(tf.day == 1);
CHECK(tf.weekday == 6); // Saturday
CHECK(tf.hours == 0);
CHECK(tf.minutes == 0);
CHECK(tf.seconds == 0);
CHECK(tf.milliseconds == 0);
}
TEST_CASE("calendar decomposition: leap day (2000-02-29)", "[chrono]") {
auto tf = decompose(kFtLeapDay);
CHECK(tf.year == 2000);
CHECK(tf.month == 2);
CHECK(tf.day == 29);
CHECK(tf.weekday == 2); // Tuesday
CHECK(tf.hours == 0);
CHECK(tf.minutes == 0);
CHECK(tf.seconds == 0);
CHECK(tf.milliseconds == 0);
}
TEST_CASE("calendar decomposition: sub-day (2020-12-30 12:00:12.345)", "[chrono]") {
auto tf = decompose(kFtSubDay);
CHECK(tf.year == 2020);
CHECK(tf.month == 12);
CHECK(tf.day == 30);
CHECK(tf.weekday == 3); // Wednesday
CHECK(tf.hours == 12);
CHECK(tf.minutes == 0);
CHECK(tf.seconds == 12);
CHECK(tf.milliseconds == 345);
}
TEST_CASE("calendar decomposition: 2021-01-01", "[chrono]") {
auto tf = decompose(kFt2021);
CHECK(tf.year == 2021);
CHECK(tf.month == 1);
CHECK(tf.day == 1);
CHECK(tf.weekday == 5); // Friday
CHECK(tf.hours == 0);
CHECK(tf.minutes == 0);
CHECK(tf.seconds == 0);
CHECK(tf.milliseconds == 0);
}
// =============================================================================
// Section 5: Calendar Recomposition (mirrors RtlTimeFieldsToTime)
// =============================================================================
// Recompose fields to FILETIME exactly as RtlTimeFieldsToTime_entry does
static uint64_t recompose(int y, unsigned m, unsigned d, int hour, int min, int sec, int ms) {
auto ymd =
std::chrono::year_month_day{std::chrono::year{y}, std::chrono::month{m}, std::chrono::day{d}};
if (!ymd.ok())
return 0;
auto dp = static_cast<std::chrono::sys_days>(ymd);
std::chrono::system_clock::time_point time = dp;
time += std::chrono::hours{hour};
time += std::chrono::minutes{min};
time += std::chrono::seconds{sec};
time += std::chrono::milliseconds{ms};
return WinSystemClock::to_file_time(WinSystemClock::from_sys(time));
}
TEST_CASE("calendar recomposition: known dates produce correct FILETIMEs", "[chrono]") {
CHECK(recompose(1601, 1, 1, 0, 0, 0, 0) == kFtNtEpoch);
CHECK(recompose(1970, 1, 1, 0, 0, 0, 0) == kFtUnixEpoch);
CHECK(recompose(2000, 1, 1, 0, 0, 0, 0) == kFtY2k);
CHECK(recompose(2000, 2, 29, 0, 0, 0, 0) == kFtLeapDay);
CHECK(recompose(2020, 12, 30, 12, 0, 12, 345) == kFtSubDay);
CHECK(recompose(2021, 1, 1, 0, 0, 0, 0) == kFt2021);
}
TEST_CASE("calendar recomposition: decompose then recompose round-trips", "[chrono]") {
for (uint64_t ft : {kFtNtEpoch, kFtUnixEpoch, kFtY2k, kFtLeapDay, kFtSubDay, kFt2021}) {
auto tf = decompose(ft);
auto result =
recompose(tf.year, tf.month, tf.day, tf.hours, tf.minutes, tf.seconds, tf.milliseconds);
CHECK(result == ft);
}
}
TEST_CASE("year_month_day::ok rejects invalid dates", "[chrono]") {
// Feb 30 - never valid
CHECK_FALSE(std::chrono::year_month_day{std::chrono::year{2000}, std::chrono::month{2},
std::chrono::day{30}}
.ok());
// Month 13 - invalid month
CHECK_FALSE(std::chrono::year_month_day{std::chrono::year{2000}, std::chrono::month{13},
std::chrono::day{1}}
.ok());
// Day 0 - invalid day
CHECK_FALSE(std::chrono::year_month_day{std::chrono::year{2000}, std::chrono::month{1},
std::chrono::day{0}}
.ok());
// Feb 29 in non-leap year
CHECK_FALSE(std::chrono::year_month_day{std::chrono::year{2001}, std::chrono::month{2},
std::chrono::day{29}}
.ok());
// Feb 29 in leap year - valid
CHECK(std::chrono::year_month_day{std::chrono::year{2000}, std::chrono::month{2},
std::chrono::day{29}}
.ok());
// Century non-leap: 1900 is not a leap year
CHECK_FALSE(std::chrono::year_month_day{std::chrono::year{1900}, std::chrono::month{2},
std::chrono::day{29}}
.ok());
// 400-year leap: 2000 is a leap year (already checked above, but explicit)
CHECK(std::chrono::year_month_day{std::chrono::year{2000}, std::chrono::month{2},
std::chrono::day{29}}
.ok());
}
TEST_CASE("recompose returns 0 for invalid dates", "[chrono]") {
CHECK(recompose(2000, 2, 30, 0, 0, 0, 0) == 0);
CHECK(recompose(2001, 2, 29, 0, 0, 0, 0) == 0);
CHECK(recompose(2000, 13, 1, 0, 0, 0, 0) == 0);
CHECK(recompose(2000, 1, 0, 0, 0, 0, 0) == 0);
}
// =============================================================================
// Section 6: Weekday c_encoding Edge Cases
// =============================================================================
TEST_CASE("weekday c_encoding returns 0=Sunday through 6=Saturday", "[chrono]") {
using namespace std::chrono;
// Verify the full range of c_encoding values using known days
// 2000-01-02 is Sunday (0)
CHECK(weekday{sys_days{year{2000} / month{1} / day{2}}}.c_encoding() == 0);
// 1601-01-01 is Monday (1)
CHECK(weekday{sys_days{year{1601} / month{1} / day{1}}}.c_encoding() == 1);
// 2000-02-29 is Tuesday (2)
CHECK(weekday{sys_days{year{2000} / month{2} / day{29}}}.c_encoding() == 2);
// 2020-12-30 is Wednesday (3)
CHECK(weekday{sys_days{year{2020} / month{12} / day{30}}}.c_encoding() == 3);
// 1970-01-01 is Thursday (4)
CHECK(weekday{sys_days{year{1970} / month{1} / day{1}}}.c_encoding() == 4);
// 2021-01-01 is Friday (5)
CHECK(weekday{sys_days{year{2021} / month{1} / day{1}}}.c_encoding() == 5);
// 2000-01-01 is Saturday (6)
CHECK(weekday{sys_days{year{2000} / month{1} / day{1}}}.c_encoding() == 6);
}