From 7c2a04ceb40ae910891a0683befa5570517c1f8f Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 12:45:09 +0200 Subject: [PATCH] test: cover rtcTest --- src/rtcTest.c | 215 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 src/rtcTest.c diff --git a/src/rtcTest.c b/src/rtcTest.c new file mode 100644 index 0000000..1756c20 --- /dev/null +++ b/src/rtcTest.c @@ -0,0 +1,215 @@ +#include "ktest.h" +#include "rtc.h" + +/* + * rtc.c gets two different kinds of coverage here, the same split + * ata.c/ataTest.c already established: pure arithmetic (the BCD conversion + * and the civil-date-to-epoch math) is tested with known, hand-verified + * values that need no hardware at all, while rtcReadDateTime/rtcNow are + * exercised against whatever CMOS RTC QEMU actually emulates, so a wiring + * mistake (wrong port, wrong status bit, missed BCD/24-hour branch) shows + * up as an implausible or non-monotonic reading here, not a mocked + * assertion. + */ + +KTEST(rtcBcdToBinaryKnownValues) +{ + KT_EQ(t, rtcBcdToBinary(0x00), 0); + KT_EQ(t, rtcBcdToBinary(0x09), 9); + KT_EQ(t, rtcBcdToBinary(0x10), 10); + KT_EQ(t, rtcBcdToBinary(0x23), 23); + KT_EQ(t, rtcBcdToBinary(0x59), 59); +} + +KTEST(rtcEpochSecondsUnixEpochIsZero) +{ + RtcDateTime dt = { .year = 1970, .month = 1, .day = 1, .hour = 0, .minute = 0, .second = 0 }; + KT_EQ(t, rtcEpochSeconds(&dt), 0); +} + +KTEST(rtcEpochSecondsTimeOfDayComponent) +{ + RtcDateTime dt = { .year = 1970, .month = 1, .day = 1, .hour = 1, .minute = 2, .second = 3 }; + KT_EQ(t, rtcEpochSeconds(&dt), 1 * 3600 + 2 * 60 + 3); +} + +KTEST(rtcEpochSecondsOrdinaryDayIsOneDayApart) +{ + RtcDateTime a = { .year = 2026, .month = 9, .day = 7, .hour = 0, .minute = 0, .second = 0 }; + RtcDateTime b = { .year = 2026, .month = 9, .day = 8, .hour = 0, .minute = 0, .second = 0 }; + KT_EQ(t, rtcEpochSeconds(&b) - rtcEpochSeconds(&a), 86400); +} + +/* The single most common bug in hand-rolled calendar math: getting the + leap-year rule wrong. 2024 is a leap year (divisible by 4, not by a + century that isn't also divisible by 400), so February has 29 days and + Feb 28 -> Mar 1 must be a two-day gap, not one. */ +KTEST(rtcEpochSecondsLeapYearFebruaryHas29Days) +{ + RtcDateTime feb28 = { .year = 2024, .month = 2, .day = 28, .hour = 0, .minute = 0, .second = 0 }; + RtcDateTime mar1 = { .year = 2024, .month = 3, .day = 1, .hour = 0, .minute = 0, .second = 0 }; + KT_EQ(t, rtcEpochSeconds(&mar1) - rtcEpochSeconds(&feb28), 2 * 86400); +} + +/* 2023 is not a leap year (not divisible by 4): the same Feb28 -> Mar1 + comparison must collapse to exactly one day, proving the non-leap branch + is not just always adding the leap-year's extra day. */ +KTEST(rtcEpochSecondsNonLeapYearFebruaryHas28Days) +{ + RtcDateTime feb28 = { .year = 2023, .month = 2, .day = 28, .hour = 0, .minute = 0, .second = 0 }; + RtcDateTime mar1 = { .year = 2023, .month = 3, .day = 1, .hour = 0, .minute = 0, .second = 0 }; + KT_EQ(t, rtcEpochSeconds(&mar1) - rtcEpochSeconds(&feb28), 86400); +} + +/* A century year is a leap year only when also divisible by 400: 2000 is + (400 | 2000), 1900 is not. This is the structural part of the rule the + simpler "divisible by 4" tests above cannot distinguish on their own. */ +KTEST(rtcEpochSecondsCenturyLeapRule) +{ + RtcDateTime feb28_2000 = { .year = 2000, .month = 2, .day = 28, .hour = 0, .minute = 0, .second = 0 }; + RtcDateTime mar1_2000 = { .year = 2000, .month = 3, .day = 1, .hour = 0, .minute = 0, .second = 0 }; + KT_EQ(t, rtcEpochSeconds(&mar1_2000) - rtcEpochSeconds(&feb28_2000), 2 * 86400); + + RtcDateTime feb28_1900 = { .year = 1900, .month = 2, .day = 28, .hour = 0, .minute = 0, .second = 0 }; + RtcDateTime mar1_1900 = { .year = 1900, .month = 3, .day = 1, .hour = 0, .minute = 0, .second = 0 }; + KT_EQ(t, rtcEpochSeconds(&mar1_1900) - rtcEpochSeconds(&feb28_1900), 86400); +} + +KTEST(rtcCivilFromEpochUnixEpochIsZero) +{ + RtcDateTime dt; + rtcCivilFromEpoch(0, &dt); + KT_EQ(t, dt.year, 1970); + KT_EQ(t, dt.month, 1); + KT_EQ(t, dt.day, 1); + KT_EQ(t, dt.hour, 0); + KT_EQ(t, dt.minute, 0); + KT_EQ(t, dt.second, 0); +} + +KTEST(rtcCivilFromEpochTimeOfDayComponent) +{ + RtcDateTime dt; + rtcCivilFromEpoch(1 * 3600 + 2 * 60 + 3, &dt); + KT_EQ(t, dt.year, 1970); + KT_EQ(t, dt.month, 1); + KT_EQ(t, dt.day, 1); + KT_EQ(t, dt.hour, 1); + KT_EQ(t, dt.minute, 2); + KT_EQ(t, dt.second, 3); +} + +/* Negative epoch (a date before 1970): -1 second must land on the very + last second of 1969, not silently truncate toward 1970-01-01 the way a + naive C division (which rounds toward zero) would. */ +KTEST(rtcCivilFromEpochNegativeBeforeEpoch) +{ + RtcDateTime dt; + rtcCivilFromEpoch(-1, &dt); + KT_EQ(t, dt.year, 1969); + KT_EQ(t, dt.month, 12); + KT_EQ(t, dt.day, 31); + KT_EQ(t, dt.hour, 23); + KT_EQ(t, dt.minute, 59); + KT_EQ(t, dt.second, 59); + + rtcCivilFromEpoch(-86400, &dt); + KT_EQ(t, dt.year, 1969); + KT_EQ(t, dt.month, 12); + KT_EQ(t, dt.day, 31); + KT_EQ(t, dt.hour, 0); + KT_EQ(t, dt.minute, 0); + KT_EQ(t, dt.second, 0); +} + +/* The exact leap-year civil date rtcEpochSecondsLeapYearFebruaryHas29Days + above already proves is one apart from March 1st -- rtcCivilFromEpoch + must recover 2024-02-29 exactly, not silently normalize it into + 2024-03-01 (the classic off-by-one a wrong leap rule would produce). */ +KTEST(rtcCivilFromEpochLeapDayRoundTrip) +{ + RtcDateTime leapDay = { .year = 2024, .month = 2, .day = 29, .hour = 12, .minute = 30, .second = 45 }; + int64 epoch = rtcEpochSeconds(&leapDay); + + RtcDateTime back; + rtcCivilFromEpoch(epoch, &back); + KT_EQ(t, back.year, 2024); + KT_EQ(t, back.month, 2); + KT_EQ(t, back.day, 29); + KT_EQ(t, back.hour, 12); + KT_EQ(t, back.minute, 30); + KT_EQ(t, back.second, 45); +} + +/* Round trip through both directions (rtcEpochSeconds then + rtcCivilFromEpoch) across a spread of dates spanning several years, + month/year boundaries, and a century-leap year -- proves the two + functions are genuine inverses of each other over a real range, not + just individually correct at a handful of hand-picked points. */ +KTEST(rtcCivilFromEpochRoundTripsAcrossManyDates) +{ + RtcDateTime samples[] = { + { 1970, 1, 1, 0, 0, 0 }, + { 1999, 12, 31, 23, 59, 59 }, + { 2000, 1, 1, 0, 0, 1 }, + { 2000, 2, 29, 6, 7, 8 }, + { 2023, 2, 28, 18, 0, 0 }, + { 2023, 3, 1, 0, 0, 0 }, + { 2026, 9, 7, 17, 30, 0 }, + { 2099, 12, 31, 23, 59, 59 }, + }; + for (uint64 i = 0; i < sizeof(samples) / sizeof(samples[0]); i++) { + int64 epoch = rtcEpochSeconds(&samples[i]); + RtcDateTime back; + rtcCivilFromEpoch(epoch, &back); + KT_EQ(t, back.year, samples[i].year); + KT_EQ(t, back.month, samples[i].month); + KT_EQ(t, back.day, samples[i].day); + KT_EQ(t, back.hour, samples[i].hour); + KT_EQ(t, back.minute, samples[i].minute); + KT_EQ(t, back.second, samples[i].second); + } +} + +KTEST(rtcReadDateTimeReturnsPlausibleFields) +{ + RtcDateTime dt; + KT_TRUE(t, rtcReadDateTime(&dt)); + KT_GE(t, dt.year, 2020); + KT_LE(t, dt.year, 2099); + KT_GE(t, dt.month, 1); + KT_LE(t, dt.month, 12); + KT_GE(t, dt.day, 1); + KT_LE(t, dt.day, 31); + KT_GE(t, dt.hour, 0); + KT_LT(t, dt.hour, 24); + KT_GE(t, dt.minute, 0); + KT_LT(t, dt.minute, 60); + KT_GE(t, dt.second, 0); + KT_LT(t, dt.second, 60); +} + +KTEST(rtcNowIsNeverNegativeOrObviouslyWrong) +{ + /* rtcEpochSecondsUnixEpochIsZero above already proves the math side + hits exactly 0 for 1970-01-01; a real read of a QEMU-emulated CMOS + clock in 2026 should be nowhere near that, so a driver that + accidentally reads all-zero registers (unplugged/uninitialized + hardware, a wrong port) is easy to tell apart from a real one here. */ + int64 now = rtcNow(); + KT_GT(t, now, 0); + /* 2020-01-01T00:00:00Z, a fixed sanity floor well before this test can + ever plausibly run. */ + KT_GT(t, now, 1577836800); +} + +KTEST(rtcNowIsMonotonicAcrossConsecutiveReads) +{ + int64 first = rtcNow(); + /* Real port I/O, not a busy-loop the compiler could fold away: two + fresh hardware reads a few microseconds apart must never see time + run backwards. */ + for (volatile int i = 0; i < 20000; i++) { } + int64 second = rtcNow(); + KT_GE(t, second, first); +}