mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-11-12 02:27:35 +00:00
Two ways the clock read hours off while the zone setting looked right. The ESP32-P4 esp-hosted boards started SNTP with configTime(0, 0, ...), which also sets TZ to UTC, so from the first Wi-Fi join every displayed time was UTC until the zone was picked again. #756 fixed this on the T-Display P4; the CrowPanel P4 10.1 still had it. It now starts SNTP with the user's zone (configTzTime) and, as on the T-Display, only pushes a COMPLETED sync into the clock instead of the stale boot time. Settings > Clock & time fills "By hand (local time)" with the clock's reading when the page opens. Picking another zone (or stepping the custom offset) left those digits in the old zone, so "Set clock" right after a zone change read them in the new zone and moved the clock by the difference. The field now refills whenever the zone is applied. The zone table and its POSIX strings move to helpers/TimeZones.h (same strings, same order) so test/test_time_zones.cpp can check every zone through the C library's localtime(): January, July and today's offsets, the US and EU DST edges to the minute, the Custom offset's sign for UTC-12..+14, and ClockFloorRTC with the zone in force (a later clock set, like the GPS re-sync every 30 minutes, moves the time and never the zone; a chip that reads behind the floor or garbage-future falls back to the system clock). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
154 lines
6.8 KiB
C++
154 lines
6.8 KiB
C++
// Host test for the time zone (#699): every picker zone's POSIX string through the C library's
|
|
// localtime(), the Custom offset's sign, and ClockFloorRTC's clock math with the zone in force
|
|
// (a later clock set, e.g. the GPS re-sync every 30 min, moves the time but never the zone).
|
|
// Build + run from the repo root (scripts/run-host-tests.sh does this):
|
|
// c++ -std=c++17 -Wall -Wextra -I src -I test/clock_floor_fakes test/test_time_zones.cpp \
|
|
// -o /tmp/t && /tmp/t
|
|
#include <assert.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/time.h>
|
|
#include <time.h>
|
|
|
|
// The system clock ClockFloorRTC mirrors every accepted set into (settimeofday needs root on a
|
|
// desktop), simulated so the test owns it.
|
|
static long g_sys = 0;
|
|
static int fake_settimeofday(const struct timeval* tv, const void*) { g_sys = (long)tv->tv_sec; return 0; }
|
|
static int fake_gettimeofday(struct timeval* tv, void*) { tv->tv_sec = g_sys; tv->tv_usec = 0; return 0; }
|
|
#define settimeofday fake_settimeofday
|
|
#define gettimeofday fake_gettimeofday
|
|
#include "helpers/ClockFloorRTC.h"
|
|
#undef settimeofday
|
|
#undef gettimeofday
|
|
|
|
#include "helpers/TimeZones.h"
|
|
|
|
// UTC offset in minutes (local - UTC) that zone string `tz` gives at epoch `t`.
|
|
static int offsetMin(const char* tz, time_t t) {
|
|
setenv("TZ", tz, 1);
|
|
tzset();
|
|
struct tm l, u;
|
|
localtime_r(&t, &l);
|
|
gmtime_r(&t, &u);
|
|
int d = (l.tm_hour - u.tm_hour) * 60 + (l.tm_min - u.tm_min);
|
|
const int dd = l.tm_yday - u.tm_yday;
|
|
if (dd == 1 || dd < -1) d += 24 * 60; // local is the next day (incl. year wrap)
|
|
if (dd == -1 || dd > 1) d -= 24 * 60;
|
|
return d;
|
|
}
|
|
static int zoneOffset(int zone, int custom_h, time_t t) {
|
|
char tz[48];
|
|
TimeZones::buildPosix(zone, custom_h, tz, sizeof tz);
|
|
return offsetMin(tz, t);
|
|
}
|
|
static int indexOf(const char* label) {
|
|
for (int i = 0; i < TimeZones::ZONE_N; i++) if (!strcmp(TimeZones::ZONES[i].label, label)) return i;
|
|
assert(!"zone missing");
|
|
return -1;
|
|
}
|
|
|
|
int main() {
|
|
// 2026 reference instants (UTC).
|
|
const time_t JAN = 1768478400; // 2026-01-15 12:00Z
|
|
const time_t JUL = 1784116800; // 2026-07-15 12:00Z
|
|
const time_t OCT26 = 1793017200; // 2026-10-26 12:20Z: EU back on winter time, US still on DST
|
|
|
|
struct { const char* label; int jan, jul, oct26; } want[] = {
|
|
{ "Europe (CET/CEST)", 60, 120, 60 },
|
|
{ "UK (GMT/BST)", 0, 60, 0 },
|
|
{ "UTC", 0, 0, 0 },
|
|
{ "US Eastern", -300, -240, -240 },
|
|
{ "US Central", -360, -300, -300 },
|
|
{ "US Mountain", -420, -360, -360 },
|
|
{ "US Arizona (no DST)",-420, -420, -420 },
|
|
{ "US Pacific", -480, -420, -420 },
|
|
{ "US Alaska", -540, -480, -480 },
|
|
{ "US Hawaii", -600, -600, -600 },
|
|
{ "Canada Atlantic", -240, -180, -180 },
|
|
{ "Brazil (Brasilia)", -180, -180, -180 },
|
|
{ "India (IST)", 330, 330, 330 },
|
|
{ "China (CST)", 480, 480, 480 },
|
|
{ "Japan (JST)", 540, 540, 540 },
|
|
{ "Sydney (AEST/AEDT)", 660, 600, 660 },
|
|
};
|
|
static_assert(sizeof(want) / sizeof(want[0]) == (size_t)TimeZones::ZONE_N, "a zone without a test row");
|
|
for (auto& w : want) {
|
|
const int z = indexOf(w.label);
|
|
assert(zoneOffset(z, 0, JAN) == w.jan);
|
|
assert(zoneOffset(z, 0, JUL) == w.jul);
|
|
assert(zoneOffset(z, 0, OCT26) == w.oct26);
|
|
}
|
|
|
|
// DST edges to the minute: US 2026-03-08 07:00Z, EU 2026-03-29 01:00Z.
|
|
const int us = indexOf("US Eastern"), eu = indexOf("Europe (CET/CEST)");
|
|
assert(zoneOffset(us, 0, 1772953200 - 60) == -300 && zoneOffset(us, 0, 1772953200) == -240);
|
|
assert(zoneOffset(eu, 0, 1774746000 - 60) == 60 && zoneOffset(eu, 0, 1774746000) == 120);
|
|
|
|
// Custom (UTC offset): UTC+N reads N hours ahead of UTC, all year, both signs.
|
|
for (int h = -12; h <= 14; h++) {
|
|
assert(zoneOffset(TimeZones::CUSTOM, h, JAN) == h * 60);
|
|
assert(zoneOffset(TimeZones::CUSTOM, h, JUL) == h * 60);
|
|
}
|
|
assert(zoneOffset(TimeZones::CUSTOM, 99, JAN) == 23 * 60); // clamped like the setter
|
|
// A stale index (an older build's longer list) reads as the default, as the saved setting does.
|
|
char tz[48];
|
|
TimeZones::buildPosix(TimeZones::COUNT + 3, 0, tz, sizeof tz);
|
|
assert(!strcmp(tz, TimeZones::ZONES[0].posix));
|
|
TimeZones::buildPosix(0, 0, tz, 4); // truncates, stays terminated
|
|
assert(strlen(tz) == 3);
|
|
|
|
// ---- ClockFloorRTC with the zone in force ----
|
|
struct Dummy : mesh::RTCClock {
|
|
uint32_t getCurrentTime() override { return 0; }
|
|
void setCurrentTime(uint32_t) override {}
|
|
} fallback;
|
|
ClockFloorRTC clk(fallback);
|
|
const uint32_t T = (uint32_t)OCT26;
|
|
|
|
// A sync (NTP / GPS / phone) lands: chip, floor and system clock all follow.
|
|
g_sys = ClockFloorRTC::MIN_VALID_EPOCH; // the power-on seed
|
|
FakeRtcChip::now() = ClockFloorRTC::MIN_VALID_EPOCH;
|
|
clk.setCurrentTime(T);
|
|
assert(FakeRtcChip::now() == T && g_sys == (long)T && clk.getCurrentTime() == T);
|
|
assert(clk.timeIsCurrent());
|
|
|
|
// The display reads the system clock through the zone: US Eastern = 08:20 local.
|
|
TimeZones::buildPosix(us, 0, tz, sizeof tz);
|
|
setenv("TZ", tz, 1); tzset();
|
|
time_t now = (time_t)g_sys; struct tm l; localtime_r(&now, &l);
|
|
assert(l.tm_hour == 8 && l.tm_min == 20);
|
|
// 30 minutes on, the GPS re-sync sets the clock again: the time moves, the zone does not.
|
|
clk.setCurrentTime(T + 1800);
|
|
assert(!strcmp(getenv("TZ"), tz));
|
|
now = (time_t)g_sys; localtime_r(&now, &l);
|
|
assert(l.tm_hour == 8 && l.tm_min == 50);
|
|
|
|
// Garbage sets are refused whatever the source (1902 GPS date, the unset seed, 2043 chip).
|
|
clk.setCurrentTime(0);
|
|
clk.setCurrentTime(ClockFloorRTC::MIN_VALID_EPOCH);
|
|
clk.setCurrentTime(ClockFloorRTC::MAX_PLAUSIBLE_EPOCH + 1);
|
|
assert(g_sys == (long)(T + 1800) && FakeRtcChip::now() == T + 1800);
|
|
|
|
// The chip reads behind the floor (a write that did not take, a stopped oscillator after
|
|
// the floor moved on) while the system clock runs: the system clock wins (#746/#756), and
|
|
// the floor follows it.
|
|
assert(clk.getCurrentTime() == T + 1800 && clk.getFloor() == T + 1800);
|
|
FakeRtcChip::now() = T; // the chip lost the last set
|
|
g_sys = T + 3600;
|
|
assert(clk.getCurrentTime() == T + 3600 && clk.getFloor() == T + 3600);
|
|
// ...and the chip asserting garbage-future is ignored the same way.
|
|
FakeRtcChip::now() = ClockFloorRTC::MAX_PLAUSIBLE_EPOCH + 100000;
|
|
g_sys = T + 3700;
|
|
assert(clk.getCurrentTime() == T + 3700);
|
|
// Both behind the floor: hold the floor (protocol timestamps never step back).
|
|
FakeRtcChip::now() = T; g_sys = T;
|
|
assert(clk.getCurrentTime() == T + 3700);
|
|
// A real correction more than 10 min back pulls the floor down with it.
|
|
clk.setCurrentTime(T);
|
|
assert(clk.getFloor() == T && clk.getCurrentTime() == T);
|
|
|
|
printf("test_time_zones: all passed (%d zones + custom)\n", TimeZones::ZONE_N);
|
|
return 0;
|
|
}
|