]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
test: add tests for format_timestamp() and parse_timestamp() with various timezone 26409/head
authorYu Watanabe <watanabe.yu+github@gmail.com>
Sat, 11 Feb 2023 20:30:49 +0000 (05:30 +0900)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Thu, 23 Feb 2023 23:55:28 +0000 (08:55 +0900)
src/test/test-time-util.c

index dee012fa2effb80608357ad4a824a15ef3515d1c..d4ba12497725695295f79eec0abaf3e5af7ee034 100644 (file)
@@ -1,6 +1,9 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
+#include "dirent-util.h"
 #include "env-util.h"
+#include "fd-util.h"
+#include "fileio.h"
 #include "random-util.h"
 #include "serialize.h"
 #include "string-util.h"
@@ -8,6 +11,8 @@
 #include "tests.h"
 #include "time-util.h"
 
+#define TRIAL 100u
+
 TEST(parse_sec) {
         usec_t u;
 
@@ -334,11 +339,11 @@ TEST(usec_sub_signed) {
 }
 
 TEST(format_timestamp) {
-        for (unsigned i = 0; i < 100; i++) {
+        for (unsigned i = 0; i < TRIAL; i++) {
                 char buf[CONST_MAX(FORMAT_TIMESTAMP_MAX, FORMAT_TIMESPAN_MAX)];
                 usec_t x, y;
 
-                x = random_u64_range(2147483600 * USEC_PER_SEC) + 1;
+                x = random_u64_range(USEC_TIMESTAMP_FORMATTABLE_MAX - USEC_PER_SEC) + USEC_PER_SEC;
 
                 assert_se(format_timestamp(buf, sizeof(buf), x));
                 log_debug("%s", buf);
@@ -384,20 +389,91 @@ TEST(format_timestamp) {
         }
 }
 
+static void test_format_timestamp_impl(usec_t x) {
+        bool success;
+        const char *xx, *yy;
+        usec_t y;
+
+        xx = FORMAT_TIMESTAMP(x);
+        assert_se(xx);
+        assert_se(parse_timestamp(xx, &y) >= 0);
+        yy = FORMAT_TIMESTAMP(y);
+        assert_se(yy);
+
+        success = (x / USEC_PER_SEC == y / USEC_PER_SEC) && streq(xx, yy);
+        log_full(success ? LOG_DEBUG : LOG_ERR, "@" USEC_FMT " → %s → @" USEC_FMT " → %s", x, xx, y, yy);
+        assert_se(x / USEC_PER_SEC == y / USEC_PER_SEC);
+        assert_se(streq(xx, yy));
+}
+
+static void test_format_timestamp_loop(void) {
+        test_format_timestamp_impl(USEC_PER_SEC);
+
+        for (unsigned i = 0; i < TRIAL; i++) {
+                usec_t x;
+
+                x = random_u64_range(USEC_TIMESTAMP_FORMATTABLE_MAX - USEC_PER_SEC) + USEC_PER_SEC;
+                test_format_timestamp_impl(x);
+        }
+}
+
 TEST(FORMAT_TIMESTAMP) {
-        for (unsigned i = 0; i < 100; i++) {
-                _cleanup_free_ char *buf;
-                usec_t x, y;
+        test_format_timestamp_loop();
+}
 
-                x = random_u64_range(2147483600 * USEC_PER_SEC) + 1;
+static void test_format_timestamp_with_tz_one(const char *name1, const char *name2) {
+        _cleanup_free_ char *buf = NULL, *tz = NULL;
+        const char *name, *saved_tz;
 
-                /* strbuf() is to test the macro in an argument to a function call. */
-                assert_se(buf = strdup(FORMAT_TIMESTAMP(x)));
-                log_debug("%s", buf);
-                assert_se(parse_timestamp(buf, &y) >= 0);
-                assert_se(x / USEC_PER_SEC == y / USEC_PER_SEC);
+        if (name2)
+                assert_se(buf = path_join(name1, name2));
+        name = buf ?: name1;
+
+        if (!timezone_is_valid(name, LOG_DEBUG))
+                return;
+
+        log_info("/* %s(%s) */", __func__, name);
+
+        saved_tz = getenv("TZ");
+
+        assert_se(tz = strjoin(":", name));
+        assert_se(setenv("TZ", tz, 1) >= 0);
+        tzset();
+        log_debug("%s: tzname[0]=%s, tzname[1]=%s", tz, strempty(tzname[0]), strempty(tzname[1]));
+
+        test_format_timestamp_loop();
+
+        assert_se(set_unset_env("TZ", saved_tz, true) == 0);
+        tzset();
+}
+
+TEST(FORMAT_TIMESTAMP_with_tz) {
+        if (!slow_tests_enabled())
+                return (void) log_tests_skipped("slow tests are disabled");
+
+        _cleanup_closedir_ DIR *dir = opendir("/usr/share/zoneinfo");
+        if (!dir)
+                return (void) log_tests_skipped_errno(errno, "Failed to open /usr/share/zoneinfo");
+
+        FOREACH_DIRENT(de, dir, break) {
+                if (de->d_type == DT_REG)
+                        test_format_timestamp_with_tz_one(de->d_name, NULL);
+
+                else if (de->d_type == DT_DIR) {
+                        if (streq(de->d_name, "right"))
+                                /* The test does not support timezone with leap second info. */
+                                continue;
 
-                assert_se(streq(FORMAT_TIMESTAMP(x), buf));
+                        _cleanup_closedir_ DIR *subdir = xopendirat(dirfd(dir), de->d_name, 0);
+                        if (!subdir) {
+                                log_notice_errno(errno, "Failed to open /usr/share/zoneinfo/%s, ignoring: %m", de->d_name);
+                                continue;
+                        }
+
+                        FOREACH_DIRENT(subde, subdir, break)
+                                if (subde->d_type == DT_REG)
+                                        test_format_timestamp_with_tz_one(de->d_name, subde->d_name);
+                }
         }
 }
 
@@ -579,6 +655,219 @@ TEST(format_timestamp_range) {
         test_format_timestamp_one(USEC_INFINITY, TIMESTAMP_UTC, NULL);
 }
 
+static void test_parse_timestamp_one(const char *str, usec_t max_diff, usec_t expected) {
+        usec_t usec;
+
+        log_debug("/* %s(%s) */", __func__, str);
+        assert_se(parse_timestamp(str, &usec) >= 0);
+        assert_se(usec >= expected);
+        assert_se(usec_sub_unsigned(usec, expected) <= max_diff);
+}
+
+TEST(parse_timestamp) {
+        usec_t today, now_usec;
+
+        /* UTC */
+        test_parse_timestamp_one("Thu 1970-01-01 00:01 UTC", 0, USEC_PER_MINUTE);
+        test_parse_timestamp_one("Thu 1970-01-01 00:00:01 UTC", 0, USEC_PER_SEC);
+        test_parse_timestamp_one("Thu 1970-01-01 00:00:01.001 UTC", 0, USEC_PER_SEC + 1000);
+        test_parse_timestamp_one("Thu 1970-01-01 00:00:01.0010 UTC", 0, USEC_PER_SEC + 1000);
+
+        test_parse_timestamp_one("Thu 70-01-01 00:01 UTC", 0, USEC_PER_MINUTE);
+        test_parse_timestamp_one("Thu 70-01-01 00:00:01 UTC", 0, USEC_PER_SEC);
+        test_parse_timestamp_one("Thu 70-01-01 00:00:01.001 UTC", 0, USEC_PER_SEC + 1000);
+        test_parse_timestamp_one("Thu 70-01-01 00:00:01.0010 UTC", 0, USEC_PER_SEC + 1000);
+
+        test_parse_timestamp_one("1970-01-01 00:01 UTC", 0, USEC_PER_MINUTE);
+        test_parse_timestamp_one("1970-01-01 00:00:01 UTC", 0, USEC_PER_SEC);
+        test_parse_timestamp_one("1970-01-01 00:00:01.001 UTC", 0, USEC_PER_SEC + 1000);
+        test_parse_timestamp_one("1970-01-01 00:00:01.0010 UTC", 0, USEC_PER_SEC + 1000);
+
+        test_parse_timestamp_one("70-01-01 00:01 UTC", 0, USEC_PER_MINUTE);
+        test_parse_timestamp_one("70-01-01 00:00:01 UTC", 0, USEC_PER_SEC);
+        test_parse_timestamp_one("70-01-01 00:00:01.001 UTC", 0, USEC_PER_SEC + 1000);
+        test_parse_timestamp_one("70-01-01 00:00:01.0010 UTC", 0, USEC_PER_SEC + 1000);
+
+        if (timezone_is_valid("Asia/Tokyo", LOG_DEBUG)) {
+                /* Asia/Tokyo (+0900) */
+                test_parse_timestamp_one("Thu 1970-01-01 09:01 Asia/Tokyo", 0, USEC_PER_MINUTE);
+                test_parse_timestamp_one("Thu 1970-01-01 09:00:01 Asia/Tokyo", 0, USEC_PER_SEC);
+                test_parse_timestamp_one("Thu 1970-01-01 09:00:01.001 Asia/Tokyo", 0, USEC_PER_SEC + 1000);
+                test_parse_timestamp_one("Thu 1970-01-01 09:00:01.0010 Asia/Tokyo", 0, USEC_PER_SEC + 1000);
+
+                test_parse_timestamp_one("Thu 70-01-01 09:01 Asia/Tokyo", 0, USEC_PER_MINUTE);
+                test_parse_timestamp_one("Thu 70-01-01 09:00:01 Asia/Tokyo", 0, USEC_PER_SEC);
+                test_parse_timestamp_one("Thu 70-01-01 09:00:01.001 Asia/Tokyo", 0, USEC_PER_SEC + 1000);
+                test_parse_timestamp_one("Thu 70-01-01 09:00:01.0010 Asia/Tokyo", 0, USEC_PER_SEC + 1000);
+
+                test_parse_timestamp_one("1970-01-01 09:01 Asia/Tokyo", 0, USEC_PER_MINUTE);
+                test_parse_timestamp_one("1970-01-01 09:00:01 Asia/Tokyo", 0, USEC_PER_SEC);
+                test_parse_timestamp_one("1970-01-01 09:00:01.001 Asia/Tokyo", 0, USEC_PER_SEC + 1000);
+                test_parse_timestamp_one("1970-01-01 09:00:01.0010 Asia/Tokyo", 0, USEC_PER_SEC + 1000);
+
+                test_parse_timestamp_one("70-01-01 09:01 Asia/Tokyo", 0, USEC_PER_MINUTE);
+                test_parse_timestamp_one("70-01-01 09:00:01 Asia/Tokyo", 0, USEC_PER_SEC);
+                test_parse_timestamp_one("70-01-01 09:00:01.001 Asia/Tokyo", 0, USEC_PER_SEC + 1000);
+                test_parse_timestamp_one("70-01-01 09:00:01.0010 Asia/Tokyo", 0, USEC_PER_SEC + 1000);
+
+                const char *saved_tz = getenv("TZ");
+                assert_se(setenv("TZ", ":Asia/Tokyo", 1) >= 0);
+
+                /* JST (+0900) */
+                test_parse_timestamp_one("Thu 1970-01-01 09:01 JST", 0, USEC_PER_MINUTE);
+                test_parse_timestamp_one("Thu 1970-01-01 09:00:01 JST", 0, USEC_PER_SEC);
+                test_parse_timestamp_one("Thu 1970-01-01 09:00:01.001 JST", 0, USEC_PER_SEC + 1000);
+                test_parse_timestamp_one("Thu 1970-01-01 09:00:01.0010 JST", 0, USEC_PER_SEC + 1000);
+
+                test_parse_timestamp_one("Thu 70-01-01 09:01 JST", 0, USEC_PER_MINUTE);
+                test_parse_timestamp_one("Thu 70-01-01 09:00:01 JST", 0, USEC_PER_SEC);
+                test_parse_timestamp_one("Thu 70-01-01 09:00:01.001 JST", 0, USEC_PER_SEC + 1000);
+                test_parse_timestamp_one("Thu 70-01-01 09:00:01.0010 JST", 0, USEC_PER_SEC + 1000);
+
+                test_parse_timestamp_one("1970-01-01 09:01 JST", 0, USEC_PER_MINUTE);
+                test_parse_timestamp_one("1970-01-01 09:00:01 JST", 0, USEC_PER_SEC);
+                test_parse_timestamp_one("1970-01-01 09:00:01.001 JST", 0, USEC_PER_SEC + 1000);
+                test_parse_timestamp_one("1970-01-01 09:00:01.0010 JST", 0, USEC_PER_SEC + 1000);
+
+                test_parse_timestamp_one("70-01-01 09:01 JST", 0, USEC_PER_MINUTE);
+                test_parse_timestamp_one("70-01-01 09:00:01 JST", 0, USEC_PER_SEC);
+                test_parse_timestamp_one("70-01-01 09:00:01.001 JST", 0, USEC_PER_SEC + 1000);
+                test_parse_timestamp_one("70-01-01 09:00:01.0010 JST", 0, USEC_PER_SEC + 1000);
+
+                assert_se(set_unset_env("TZ", saved_tz, true) == 0);
+        }
+
+        if (timezone_is_valid("America/New_York", LOG_DEBUG)) {
+                /* America/New_York (-0500) */
+                test_parse_timestamp_one("Wed 1969-12-31 19:01 America/New_York", 0, USEC_PER_MINUTE);
+                test_parse_timestamp_one("Wed 1969-12-31 19:00:01 America/New_York", 0, USEC_PER_SEC);
+                test_parse_timestamp_one("Wed 1969-12-31 19:00:01.001 America/New_York", 0, USEC_PER_SEC + 1000);
+                test_parse_timestamp_one("Wed 1969-12-31 19:00:01.0010 America/New_York", 0, USEC_PER_SEC + 1000);
+
+                test_parse_timestamp_one("Wed 69-12-31 19:01 America/New_York", 0, USEC_PER_MINUTE);
+                test_parse_timestamp_one("Wed 69-12-31 19:00:01 America/New_York", 0, USEC_PER_SEC);
+                test_parse_timestamp_one("Wed 69-12-31 19:00:01.001 America/New_York", 0, USEC_PER_SEC + 1000);
+                test_parse_timestamp_one("Wed 69-12-31 19:00:01.0010 America/New_York", 0, USEC_PER_SEC + 1000);
+
+                test_parse_timestamp_one("1969-12-31 19:01 America/New_York", 0, USEC_PER_MINUTE);
+                test_parse_timestamp_one("1969-12-31 19:00:01 America/New_York", 0, USEC_PER_SEC);
+                test_parse_timestamp_one("1969-12-31 19:00:01.001 America/New_York", 0, USEC_PER_SEC + 1000);
+                test_parse_timestamp_one("1969-12-31 19:00:01.0010 America/New_York", 0, USEC_PER_SEC + 1000);
+
+                test_parse_timestamp_one("69-12-31 19:01 America/New_York", 0, USEC_PER_MINUTE);
+                test_parse_timestamp_one("69-12-31 19:00:01 America/New_York", 0, USEC_PER_SEC);
+                test_parse_timestamp_one("69-12-31 19:00:01.001 America/New_York", 0, USEC_PER_SEC + 1000);
+                test_parse_timestamp_one("69-12-31 19:00:01.0010 America/New_York", 0, USEC_PER_SEC + 1000);
+
+                const char *saved_tz = getenv("TZ");
+                assert_se(setenv("TZ", ":America/New_York", 1) >= 0);
+
+                /* EST (-0500) */
+                test_parse_timestamp_one("Wed 1969-12-31 19:01 EST", 0, USEC_PER_MINUTE);
+                test_parse_timestamp_one("Wed 1969-12-31 19:00:01 EST", 0, USEC_PER_SEC);
+                test_parse_timestamp_one("Wed 1969-12-31 19:00:01.001 EST", 0, USEC_PER_SEC + 1000);
+                test_parse_timestamp_one("Wed 1969-12-31 19:00:01.0010 EST", 0, USEC_PER_SEC + 1000);
+
+                test_parse_timestamp_one("Wed 69-12-31 19:01 EST", 0, USEC_PER_MINUTE);
+                test_parse_timestamp_one("Wed 69-12-31 19:00:01 EST", 0, USEC_PER_SEC);
+                test_parse_timestamp_one("Wed 69-12-31 19:00:01.001 EST", 0, USEC_PER_SEC + 1000);
+                test_parse_timestamp_one("Wed 69-12-31 19:00:01.0010 EST", 0, USEC_PER_SEC + 1000);
+
+                test_parse_timestamp_one("1969-12-31 19:01 EST", 0, USEC_PER_MINUTE);
+                test_parse_timestamp_one("1969-12-31 19:00:01 EST", 0, USEC_PER_SEC);
+                test_parse_timestamp_one("1969-12-31 19:00:01.001 EST", 0, USEC_PER_SEC + 1000);
+                test_parse_timestamp_one("1969-12-31 19:00:01.0010 EST", 0, USEC_PER_SEC + 1000);
+
+                test_parse_timestamp_one("69-12-31 19:01 EST", 0, USEC_PER_MINUTE);
+                test_parse_timestamp_one("69-12-31 19:00:01 EST", 0, USEC_PER_SEC);
+                test_parse_timestamp_one("69-12-31 19:00:01.001 EST", 0, USEC_PER_SEC + 1000);
+                test_parse_timestamp_one("69-12-31 19:00:01.0010 EST", 0, USEC_PER_SEC + 1000);
+
+                assert_se(set_unset_env("TZ", saved_tz, true) == 0);
+        }
+
+        /* -06 */
+        test_parse_timestamp_one("Wed 1969-12-31 18:01 -06", 0, USEC_PER_MINUTE);
+        test_parse_timestamp_one("Wed 1969-12-31 18:00:01 -06", 0, USEC_PER_SEC);
+        test_parse_timestamp_one("Wed 1969-12-31 18:00:01.001 -06", 0, USEC_PER_SEC + 1000);
+        test_parse_timestamp_one("Wed 1969-12-31 18:00:01.0010 -06", 0, USEC_PER_SEC + 1000);
+
+        test_parse_timestamp_one("Wed 69-12-31 18:01 -06", 0, USEC_PER_MINUTE);
+        test_parse_timestamp_one("Wed 69-12-31 18:00:01 -06", 0, USEC_PER_SEC);
+        test_parse_timestamp_one("Wed 69-12-31 18:00:01.001 -06", 0, USEC_PER_SEC + 1000);
+        test_parse_timestamp_one("Wed 69-12-31 18:00:01.0010 -06", 0, USEC_PER_SEC + 1000);
+
+        test_parse_timestamp_one("1969-12-31 18:01 -06", 0, USEC_PER_MINUTE);
+        test_parse_timestamp_one("1969-12-31 18:00:01 -06", 0, USEC_PER_SEC);
+        test_parse_timestamp_one("1969-12-31 18:00:01.001 -06", 0, USEC_PER_SEC + 1000);
+        test_parse_timestamp_one("1969-12-31 18:00:01.0010 -06", 0, USEC_PER_SEC + 1000);
+
+        test_parse_timestamp_one("69-12-31 18:01 -06", 0, USEC_PER_MINUTE);
+        test_parse_timestamp_one("69-12-31 18:00:01 -06", 0, USEC_PER_SEC);
+        test_parse_timestamp_one("69-12-31 18:00:01.001 -06", 0, USEC_PER_SEC + 1000);
+        test_parse_timestamp_one("69-12-31 18:00:01.0010 -06", 0, USEC_PER_SEC + 1000);
+
+        /* -0600 */
+        test_parse_timestamp_one("Wed 1969-12-31 18:01 -0600", 0, USEC_PER_MINUTE);
+        test_parse_timestamp_one("Wed 1969-12-31 18:00:01 -0600", 0, USEC_PER_SEC);
+        test_parse_timestamp_one("Wed 1969-12-31 18:00:01.001 -0600", 0, USEC_PER_SEC + 1000);
+        test_parse_timestamp_one("Wed 1969-12-31 18:00:01.0010 -0600", 0, USEC_PER_SEC + 1000);
+
+        test_parse_timestamp_one("Wed 69-12-31 18:01 -0600", 0, USEC_PER_MINUTE);
+        test_parse_timestamp_one("Wed 69-12-31 18:00:01 -0600", 0, USEC_PER_SEC);
+        test_parse_timestamp_one("Wed 69-12-31 18:00:01.001 -0600", 0, USEC_PER_SEC + 1000);
+        test_parse_timestamp_one("Wed 69-12-31 18:00:01.0010 -0600", 0, USEC_PER_SEC + 1000);
+
+        test_parse_timestamp_one("1969-12-31 18:01 -0600", 0, USEC_PER_MINUTE);
+        test_parse_timestamp_one("1969-12-31 18:00:01 -0600", 0, USEC_PER_SEC);
+        test_parse_timestamp_one("1969-12-31 18:00:01.001 -0600", 0, USEC_PER_SEC + 1000);
+        test_parse_timestamp_one("1969-12-31 18:00:01.0010 -0600", 0, USEC_PER_SEC + 1000);
+
+        test_parse_timestamp_one("69-12-31 18:01 -0600", 0, USEC_PER_MINUTE);
+        test_parse_timestamp_one("69-12-31 18:00:01 -0600", 0, USEC_PER_SEC);
+        test_parse_timestamp_one("69-12-31 18:00:01.001 -0600", 0, USEC_PER_SEC + 1000);
+        test_parse_timestamp_one("69-12-31 18:00:01.0010 -0600", 0, USEC_PER_SEC + 1000);
+
+        /* -06:00 */
+        test_parse_timestamp_one("Wed 1969-12-31 18:01 -06:00", 0, USEC_PER_MINUTE);
+        test_parse_timestamp_one("Wed 1969-12-31 18:00:01 -06:00", 0, USEC_PER_SEC);
+        test_parse_timestamp_one("Wed 1969-12-31 18:00:01.001 -06:00", 0, USEC_PER_SEC + 1000);
+        test_parse_timestamp_one("Wed 1969-12-31 18:00:01.0010 -06:00", 0, USEC_PER_SEC + 1000);
+
+        test_parse_timestamp_one("Wed 69-12-31 18:01 -06:00", 0, USEC_PER_MINUTE);
+        test_parse_timestamp_one("Wed 69-12-31 18:00:01 -06:00", 0, USEC_PER_SEC);
+        test_parse_timestamp_one("Wed 69-12-31 18:00:01.001 -06:00", 0, USEC_PER_SEC + 1000);
+        test_parse_timestamp_one("Wed 69-12-31 18:00:01.0010 -06:00", 0, USEC_PER_SEC + 1000);
+
+        test_parse_timestamp_one("1969-12-31 18:01 -06:00", 0, USEC_PER_MINUTE);
+        test_parse_timestamp_one("1969-12-31 18:00:01 -06:00", 0, USEC_PER_SEC);
+        test_parse_timestamp_one("1969-12-31 18:00:01.001 -06:00", 0, USEC_PER_SEC + 1000);
+        test_parse_timestamp_one("1969-12-31 18:00:01.0010 -06:00", 0, USEC_PER_SEC + 1000);
+
+        test_parse_timestamp_one("69-12-31 18:01 -06:00", 0, USEC_PER_MINUTE);
+        test_parse_timestamp_one("69-12-31 18:00:01 -06:00", 0, USEC_PER_SEC);
+        test_parse_timestamp_one("69-12-31 18:00:01.001 -06:00", 0, USEC_PER_SEC + 1000);
+        test_parse_timestamp_one("69-12-31 18:00:01.0010 -06:00", 0, USEC_PER_SEC + 1000);
+
+        /* without date */
+        assert_se(parse_timestamp("today", &today) == 0);
+        test_parse_timestamp_one("00:01", 0, today + USEC_PER_MINUTE);
+        test_parse_timestamp_one("00:00:01", 0, today + USEC_PER_SEC);
+        test_parse_timestamp_one("00:00:01.001", 0, today + USEC_PER_SEC + 1000);
+        test_parse_timestamp_one("00:00:01.0010", 0, today + USEC_PER_SEC + 1000);
+        test_parse_timestamp_one("tomorrow", 0, today + USEC_PER_DAY);
+        test_parse_timestamp_one("yesterday", 0, today - USEC_PER_DAY);
+
+        /* relative */
+        assert_se(parse_timestamp("now", &now_usec) == 0);
+        test_parse_timestamp_one("+5hours", USEC_PER_MINUTE, now_usec + 5 * USEC_PER_HOUR);
+        if (now_usec >= 10 * USEC_PER_DAY)
+                test_parse_timestamp_one("-10days", USEC_PER_MINUTE, now_usec - 10 * USEC_PER_DAY);
+        test_parse_timestamp_one("2weeks left", USEC_PER_MINUTE, now_usec + 2 * USEC_PER_WEEK);
+        if (now_usec >= 30 * USEC_PER_MINUTE)
+                test_parse_timestamp_one("30minutes ago", USEC_PER_MINUTE, now_usec - 30 * USEC_PER_MINUTE);
+}
+
 TEST(deserialize_dual_timestamp) {
         int r;
         dual_timestamp t;
@@ -702,6 +991,71 @@ TEST(map_clock_usec) {
         }
 }
 
+static void test_timezone_offset_change_one(const char *utc, const char *pretty) {
+        usec_t x, y, z;
+        char *s;
+
+        assert_se(parse_timestamp(utc, &x) >= 0);
+
+        s = FORMAT_TIMESTAMP_STYLE(x, TIMESTAMP_UTC);
+        assert_se(parse_timestamp(s, &y) >= 0);
+        log_debug("%s -> " USEC_FMT " -> %s -> " USEC_FMT, utc, x, s, y);
+        assert_se(streq(s, utc));
+        assert_se(x == y);
+
+        assert_se(parse_timestamp(pretty, &y) >= 0);
+        s = FORMAT_TIMESTAMP_STYLE(y, TIMESTAMP_PRETTY);
+        assert_se(parse_timestamp(s, &z) >= 0);
+        log_debug("%s -> " USEC_FMT " -> %s -> " USEC_FMT, pretty, y, s, z);
+        assert_se(streq(s, pretty));
+        assert_se(x == y);
+        assert_se(x == z);
+}
+
+TEST(timezone_offset_change) {
+        const char *tz = getenv("TZ");
+
+        /* See issue #26370. */
+
+        if (timezone_is_valid("Africa/Casablanca", LOG_DEBUG)) {
+                assert_se(setenv("TZ", ":Africa/Casablanca", 1) >= 0);
+                tzset();
+                log_debug("Africa/Casablanca: tzname[0]=%s, tzname[1]=%s", strempty(tzname[0]), strempty(tzname[1]));
+
+                test_timezone_offset_change_one("Sun 2015-10-25 01:59:59 UTC", "Sun 2015-10-25 02:59:59 +01");
+                test_timezone_offset_change_one("Sun 2015-10-25 02:00:00 UTC", "Sun 2015-10-25 02:00:00 +00");
+                test_timezone_offset_change_one("Sun 2018-06-17 01:59:59 UTC", "Sun 2018-06-17 01:59:59 +00");
+                test_timezone_offset_change_one("Sun 2018-06-17 02:00:00 UTC", "Sun 2018-06-17 03:00:00 +01");
+                test_timezone_offset_change_one("Sun 2018-10-28 01:59:59 UTC", "Sun 2018-10-28 02:59:59 +01");
+                test_timezone_offset_change_one("Sun 2018-10-28 02:00:00 UTC", "Sun 2018-10-28 03:00:00 +01");
+        }
+
+        if (timezone_is_valid("Asia/Atyrau", LOG_DEBUG)) {
+                assert_se(setenv("TZ", ":Asia/Atyrau", 1) >= 0);
+                tzset();
+                log_debug("Asia/Atyrau: tzname[0]=%s, tzname[1]=%s", strempty(tzname[0]), strempty(tzname[1]));
+
+                test_timezone_offset_change_one("Sat 2004-03-27 21:59:59 UTC", "Sun 2004-03-28 01:59:59 +04");
+                test_timezone_offset_change_one("Sat 2004-03-27 22:00:00 UTC", "Sun 2004-03-28 03:00:00 +05");
+                test_timezone_offset_change_one("Sat 2004-10-30 21:59:59 UTC", "Sun 2004-10-31 02:59:59 +05");
+                test_timezone_offset_change_one("Sat 2004-10-30 22:00:00 UTC", "Sun 2004-10-31 03:00:00 +05");
+        }
+
+        if (timezone_is_valid("Chile/EasterIsland", LOG_DEBUG)) {
+                assert_se(setenv("TZ", ":Chile/EasterIsland", 1) >= 0);
+                tzset();
+                log_debug("Chile/EasterIsland: tzname[0]=%s, tzname[1]=%s", strempty(tzname[0]), strempty(tzname[1]));
+
+                test_timezone_offset_change_one("Sun 1981-10-11 03:59:59 UTC", "Sat 1981-10-10 20:59:59 -07");
+                test_timezone_offset_change_one("Sun 1981-10-11 04:00:00 UTC", "Sat 1981-10-10 22:00:00 -06");
+                test_timezone_offset_change_one("Sun 1982-03-14 02:59:59 UTC", "Sat 1982-03-13 20:59:59 -06");
+                test_timezone_offset_change_one("Sun 1982-03-14 03:00:00 UTC", "Sat 1982-03-13 21:00:00 -06");
+        }
+
+        assert_se(set_unset_env("TZ", tz, true) == 0);
+        tzset();
+}
+
 static int intro(void) {
         log_info("realtime=" USEC_FMT "\n"
                  "monotonic=" USEC_FMT "\n"