This patch adds a unit test that verifies that gmtime_r and localtime_r are MT-safe.
2024-02-11 Bruno Haible <br...@clisp.org> time_r: Add tests. * lib/time_r.c: Add comment. * tests/test-gmtime_r.c: New file. * tests/test-gmtime_r-mt.c: New file, based on tests/test-nl_langinfo-mt.c. * tests/test-localtime_r.c: New file. * tests/test-localtime_r-mt.c: New file, based on tests/test-nl_langinfo-mt.c. * modules/time_r-tests: New file.
From 10321db3cd2d0883f085f10eb25f99aa80a97075 Mon Sep 17 00:00:00 2001 From: Bruno Haible <br...@clisp.org> Date: Sun, 11 Feb 2024 11:23:14 +0100 Subject: [PATCH] time_r: Add tests. * lib/time_r.c: Add comment. * tests/test-gmtime_r.c: New file. * tests/test-gmtime_r-mt.c: New file, based on tests/test-nl_langinfo-mt.c. * tests/test-localtime_r.c: New file. * tests/test-localtime_r-mt.c: New file, based on tests/test-nl_langinfo-mt.c. * modules/time_r-tests: New file. --- ChangeLog | 12 +++ lib/time_r.c | 5 ++ modules/time_r-tests | 27 ++++++ tests/test-gmtime_r-mt.c | 123 ++++++++++++++++++++++++++ tests/test-gmtime_r.c | 56 ++++++++++++ tests/test-localtime_r-mt.c | 139 ++++++++++++++++++++++++++++++ tests/test-localtime_r.c | 167 ++++++++++++++++++++++++++++++++++++ 7 files changed, 529 insertions(+) create mode 100644 modules/time_r-tests create mode 100644 tests/test-gmtime_r-mt.c create mode 100644 tests/test-gmtime_r.c create mode 100644 tests/test-localtime_r-mt.c create mode 100644 tests/test-localtime_r.c diff --git a/ChangeLog b/ChangeLog index 8b6c726087..a25e9f8706 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,15 @@ +2024-02-11 Bruno Haible <br...@clisp.org> + + time_r: Add tests. + * lib/time_r.c: Add comment. + * tests/test-gmtime_r.c: New file. + * tests/test-gmtime_r-mt.c: New file, based on + tests/test-nl_langinfo-mt.c. + * tests/test-localtime_r.c: New file. + * tests/test-localtime_r-mt.c: New file, based on + tests/test-nl_langinfo-mt.c. + * modules/time_r-tests: New file. + 2024-02-10 Paul Eggert <egg...@cs.ucla.edu> doc: improve warnings about ctime etc. diff --git a/lib/time_r.c b/lib/time_r.c index 3ef0b36802..b724f3b38d 100644 --- a/lib/time_r.c +++ b/lib/time_r.c @@ -21,6 +21,11 @@ #include <time.h> +/* The replacement functions in this file are only used on native Windows. + They are multithread-safe, because the gmtime() and localtime() functions + on native Windows — both in the ucrt and in the older MSVCRT — return a + pointer to a 'struct tm' in thread-local memory. */ + static struct tm * copy_tm_result (struct tm *dest, struct tm const *src) { diff --git a/modules/time_r-tests b/modules/time_r-tests new file mode 100644 index 0000000000..56346c0f7c --- /dev/null +++ b/modules/time_r-tests @@ -0,0 +1,27 @@ +Files: +tests/test-gmtime_r.c +tests/test-gmtime_r-mt.c +tests/test-localtime_r.c +tests/test-localtime_r-mt.c +tests/macros.h + +Depends-on: +setenv +thread +nanosleep + +configure.ac: +dnl Possibly define HAVE_STRUCT_TM_TM_ZONE. +AC_REQUIRE([AC_STRUCT_TIMEZONE]) +dnl Possibly define HAVE_STRUCT_TM_TM_GMTOFF. +AC_CHECK_MEMBERS([struct tm.tm_gmtoff],,,[[#include <time.h>]]) + +Makefile.am: +TESTS += \ + test-gmtime_r test-gmtime_r-mt \ + test-localtime_r test-localtime_r-mt +check_PROGRAMS += \ + test-gmtime_r test-gmtime_r-mt \ + test-localtime_r test-localtime_r-mt +test_gmtime_r_mt_LDADD = $(LDADD) $(LIBMULTITHREAD) $(NANOSLEEP_LIB) +test_localtime_r_mt_LDADD = $(LDADD) $(LIBMULTITHREAD) $(NANOSLEEP_LIB) diff --git a/tests/test-gmtime_r-mt.c b/tests/test-gmtime_r-mt.c new file mode 100644 index 0000000000..4d38bad1a3 --- /dev/null +++ b/tests/test-gmtime_r-mt.c @@ -0,0 +1,123 @@ +/* Multithread-safety test for gmtime_r(). + Copyright (C) 2024 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. */ + +/* Written by Bruno Haible <br...@clisp.org>, 2024. */ + +#include <config.h> + +/* Work around GCC bug 44511. */ +#if 4 < __GNUC__ + (3 <= __GNUC_MINOR__) +# pragma GCC diagnostic ignored "-Wreturn-type" +#endif + +#if USE_ISOC_THREADS || USE_POSIX_THREADS || USE_ISOC_AND_POSIX_THREADS || USE_WINDOWS_THREADS + +/* Specification. */ +#include <time.h> + +#include <stdio.h> +#include <stdlib.h> + +#include "glthread/thread.h" +#include "macros.h" + +static void * +thread1_func (void *arg) +{ + for (;;) + { + time_t t = 1509000003; /* 2017-10-26 06:40:03 */ + struct tm tm; + struct tm *result = gmtime_r (&t, &tm); + ASSERT (result == &tm); + if (!(result->tm_sec == 3 + && result->tm_min == 40 + && result->tm_hour == 6 + && result->tm_mday == 26 + && result->tm_mon == 10 - 1 + && result->tm_year == 2017 - 1900 + && result->tm_wday == 4 + && result->tm_yday == 298 + && result->tm_isdst == 0)) + { + fprintf (stderr, "thread1 disturbed by thread2!\n"); fflush (stderr); + abort (); + } + } + + /*NOTREACHED*/ +} + +static void * +thread2_func (void *arg) +{ + for (;;) + { + time_t t = 2000050005; /* 2033-05-18 17:26:45 */ + struct tm tm; + struct tm *result = gmtime_r (&t, &tm); + ASSERT (result == &tm); + if (!(result->tm_sec == 45 + && result->tm_min == 26 + && result->tm_hour == 17 + && result->tm_mday == 18 + && result->tm_mon == 5 - 1 + && result->tm_year == 2033 - 1900 + && result->tm_wday == 3 + && result->tm_yday == 137 + && result->tm_isdst == 0)) + { + fprintf (stderr, "thread2 disturbed by thread1!\n"); fflush (stderr); + abort (); + } + } + + /*NOTREACHED*/ +} + +int +main (int argc, char *argv[]) +{ + /* Create the threads. */ + gl_thread_create (thread1_func, NULL); + gl_thread_create (thread2_func, NULL); + + /* Let them run for 1 second. */ + { + struct timespec duration; + duration.tv_sec = (argc > 1 ? atoi (argv[1]) : 1); + duration.tv_nsec = 0; + + nanosleep (&duration, NULL); + } + + return 0; +} + +#else + +/* No multithreading available. */ + +#include <stdio.h> + +int +main () +{ + fputs ("Skipping test: multithreading not enabled\n", stderr); + return 77; +} + +#endif diff --git a/tests/test-gmtime_r.c b/tests/test-gmtime_r.c new file mode 100644 index 0000000000..01eebf3c29 --- /dev/null +++ b/tests/test-gmtime_r.c @@ -0,0 +1,56 @@ +/* Test gmtime_r(). + Copyright (C) 2024 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. */ + +/* Written by Bruno Haible <br...@clisp.org>, 2024. */ + +#include <config.h> + +/* Specification. */ +#include <time.h> + +#include <string.h> + +#include "macros.h" + +int +main (void) +{ + { + time_t t = 1509000003; /* 2017-10-26 06:40:03 */ + struct tm tm; + struct tm *result = gmtime_r (&t, &tm); + ASSERT (result == &tm); + ASSERT (result->tm_sec == 3); + ASSERT (result->tm_min == 40); + ASSERT (result->tm_hour == 6); + ASSERT (result->tm_mday == 26); + ASSERT (result->tm_mon == 10 - 1); + ASSERT (result->tm_year == 2017 - 1900); + ASSERT (result->tm_wday == 4); + ASSERT (result->tm_yday == 298); + ASSERT (result->tm_isdst == 0); +#if HAVE_STRUCT_TM_TM_GMTOFF /* glibc, musl, macOS, FreeBSD, NetBSD, OpenBSD, Minix, Cygwin, Android */ + ASSERT (result->tm_gmtoff == 0); +#endif +#if HAVE_STRUCT_TM_TM_ZONE /* glibc, musl, macOS, FreeBSD, NetBSD, OpenBSD, Minix, Cygwin, Android */ + printf ("tm_zone = %s\n", result->tm_zone == NULL ? "(null)" : result->tm_zone); + ASSERT (strcmp (result->tm_zone, "GMT") == 0 /* glibc, NetBSD, OpenBSD, Minix, Cygwin, Android */ + || strcmp (result->tm_zone, "UTC") == 0 /* musl, macOS, FreeBSD */); +#endif + } + + return 0; +} diff --git a/tests/test-localtime_r-mt.c b/tests/test-localtime_r-mt.c new file mode 100644 index 0000000000..ae371a8946 --- /dev/null +++ b/tests/test-localtime_r-mt.c @@ -0,0 +1,139 @@ +/* Multithread-safety test for localtime_r(). + Copyright (C) 2024 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. */ + +/* Written by Bruno Haible <br...@clisp.org>, 2024. */ + +#include <config.h> + +/* Work around GCC bug 44511. */ +#if 4 < __GNUC__ + (3 <= __GNUC_MINOR__) +# pragma GCC diagnostic ignored "-Wreturn-type" +#endif + +#if USE_ISOC_THREADS || USE_POSIX_THREADS || USE_ISOC_AND_POSIX_THREADS || USE_WINDOWS_THREADS + +/* Specification. */ +#include <time.h> + +#include <stdio.h> +#include <stdlib.h> + +#include "glthread/thread.h" +#include "macros.h" + + +/* Some common time zone name. */ + +#if defined _WIN32 && !defined __CYGWIN__ +/* Cf. <https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/default-time-zones> + or <https://ss64.com/timezones.html> */ +# define FRENCH_TZ "Romance Standard Time" +#else +/* Cf. <https://en.wikipedia.org/wiki/List_of_tz_database_time_zones> */ +# define FRENCH_TZ "Europe/Paris" +#endif + + +static void * +thread1_func (void *arg) +{ + for (;;) + { + + time_t t = 1178467200; /* 2007-05-06 18:00:00 */ + struct tm tm; + struct tm *result = localtime_r (&t, &tm); + ASSERT (result == &tm); + if (!(result->tm_sec == 0 + && result->tm_min == 0 + && result->tm_hour == 18 + && result->tm_mday == 6 + && result->tm_mon == 5 - 1 + && result->tm_year == 2007 - 1900 + && result->tm_wday == 0 + && result->tm_yday == 125 + && result->tm_isdst == 1)) + { + fprintf (stderr, "thread1 disturbed by thread2!\n"); fflush (stderr); + abort (); + } + } + + /*NOTREACHED*/ +} + +static void * +thread2_func (void *arg) +{ + for (;;) + { + time_t t = 1336320000; /* 2012-05-06 18:00:00 */ + struct tm tm; + struct tm *result = localtime_r (&t, &tm); + ASSERT (result == &tm); + if (!(result->tm_sec == 0 + && result->tm_min == 0 + && result->tm_hour == 18 + && result->tm_mday == 6 + && result->tm_mon == 5 - 1 + && result->tm_year == 2012 - 1900 + && result->tm_wday == 0 + && result->tm_yday == 126 + && result->tm_isdst == 1)) + { + fprintf (stderr, "thread2 disturbed by thread1!\n"); fflush (stderr); + abort (); + } + } + + /*NOTREACHED*/ +} + +int +main (int argc, char *argv[]) +{ + setenv ("TZ", FRENCH_TZ, 1); + + /* Create the threads. */ + gl_thread_create (thread1_func, NULL); + gl_thread_create (thread2_func, NULL); + + /* Let them run for 1 second. */ + { + struct timespec duration; + duration.tv_sec = (argc > 1 ? atoi (argv[1]) : 1); + duration.tv_nsec = 0; + + nanosleep (&duration, NULL); + } + + return 0; +} + +#else + +/* No multithreading available. */ + +#include <stdio.h> + +int +main () +{ + fputs ("Skipping test: multithreading not enabled\n", stderr); + return 77; +} + +#endif diff --git a/tests/test-localtime_r.c b/tests/test-localtime_r.c new file mode 100644 index 0000000000..70ec3b5d4f --- /dev/null +++ b/tests/test-localtime_r.c @@ -0,0 +1,167 @@ +/* Test localtime_r(). + Copyright (C) 2024 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. */ + +/* Written by Bruno Haible <br...@clisp.org>, 2024. */ + +#include <config.h> + +/* Specification. */ +#include <time.h> + +#include <string.h> + +#include "macros.h" + + +/* Some common time zone name. */ + +#if defined _WIN32 && !defined __CYGWIN__ +/* Cf. <https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/default-time-zones> + or <https://ss64.com/timezones.html> */ +# define FRENCH_TZ "Romance Standard Time" +#else +/* Cf. <https://en.wikipedia.org/wiki/List_of_tz_database_time_zones> */ +# define FRENCH_TZ "Europe/Paris" +#endif + + +int +main (void) +{ + setenv ("TZ", FRENCH_TZ, 1); + + /* Note: The result->tm_gmtoff values and the result->tm_zone values are the + same (3600, "CET" or 7200, "CEST") across all tested platforms: + glibc, musl, macOS, FreeBSD, NetBSD, OpenBSD, Minix, Cygwin, Android. */ + + /* A time point when DST was in effect. */ + { + time_t t = 1178467200; /* 2007-05-06 18:00:00 */ + struct tm tm; + struct tm *result = localtime_r (&t, &tm); + ASSERT (result == &tm); + ASSERT (result->tm_sec == 0); + ASSERT (result->tm_min == 0); + ASSERT (result->tm_hour == 18); + ASSERT (result->tm_mday == 6); + ASSERT (result->tm_mon == 5 - 1); + ASSERT (result->tm_year == 2007 - 1900); + ASSERT (result->tm_wday == 0); + ASSERT (result->tm_yday == 125); + ASSERT (result->tm_isdst == 1); +#if HAVE_STRUCT_TM_TM_GMTOFF + ASSERT (result->tm_gmtoff == (1 + result->tm_isdst) * 3600); +#endif +#if HAVE_STRUCT_TM_TM_ZONE + printf ("tm_zone = %s\n", result->tm_zone == NULL ? "(null)" : result->tm_zone); + ASSERT (strcmp (result->tm_zone, "CEST") == 0); +#endif + } + + /* 1 second before and 1 second after the DST interval started. */ + { + time_t t = 1174784399; /* 2007-03-25 01:59:59 */ + struct tm tm; + struct tm *result = localtime_r (&t, &tm); + ASSERT (result == &tm); + ASSERT (result->tm_sec == 59); + ASSERT (result->tm_min == 59); + ASSERT (result->tm_hour == 1); + ASSERT (result->tm_mday == 25); + ASSERT (result->tm_mon == 3 - 1); + ASSERT (result->tm_year == 2007 - 1900); + ASSERT (result->tm_wday == 0); + ASSERT (result->tm_yday == 83); + ASSERT (result->tm_isdst == 0); +#if HAVE_STRUCT_TM_TM_GMTOFF + ASSERT (result->tm_gmtoff == (1 + result->tm_isdst) * 3600); +#endif +#if HAVE_STRUCT_TM_TM_ZONE + printf ("tm_zone = %s\n", result->tm_zone == NULL ? "(null)" : result->tm_zone); + ASSERT (strcmp (result->tm_zone, "CET") == 0); +#endif + } + { + time_t t = 1174784401; /* 2007-03-25 03:00:01 */ + struct tm tm; + struct tm *result = localtime_r (&t, &tm); + ASSERT (result == &tm); + ASSERT (result->tm_sec == 1); + ASSERT (result->tm_min == 0); + ASSERT (result->tm_hour == 3); + ASSERT (result->tm_mday == 25); + ASSERT (result->tm_mon == 3 - 1); + ASSERT (result->tm_year == 2007 - 1900); + ASSERT (result->tm_wday == 0); + ASSERT (result->tm_yday == 83); + ASSERT (result->tm_isdst == 1); +#if HAVE_STRUCT_TM_TM_GMTOFF + ASSERT (result->tm_gmtoff == (1 + result->tm_isdst) * 3600); +#endif +#if HAVE_STRUCT_TM_TM_ZONE + printf ("tm_zone = %s\n", result->tm_zone == NULL ? "(null)" : result->tm_zone); + ASSERT (strcmp (result->tm_zone, "CEST") == 0); +#endif + } + + /* 1 second before and 1 second after the DST interval ended. */ + { + time_t t = 1193533199; /* 2007-10-28 02:59:59 */ + struct tm tm; + struct tm *result = localtime_r (&t, &tm); + ASSERT (result == &tm); + ASSERT (result->tm_sec == 59); + ASSERT (result->tm_min == 59); + ASSERT (result->tm_hour == 2); + ASSERT (result->tm_mday == 28); + ASSERT (result->tm_mon == 10 - 1); + ASSERT (result->tm_year == 2007 - 1900); + ASSERT (result->tm_wday == 0); + ASSERT (result->tm_yday == 300); + ASSERT (result->tm_isdst == 1); +#if HAVE_STRUCT_TM_TM_GMTOFF + ASSERT (result->tm_gmtoff == (1 + result->tm_isdst) * 3600); +#endif +#if HAVE_STRUCT_TM_TM_ZONE + printf ("tm_zone = %s\n", result->tm_zone == NULL ? "(null)" : result->tm_zone); + ASSERT (strcmp (result->tm_zone, "CEST") == 0); +#endif + } + { + time_t t = 1193533201; /* 2007-10-28 02:00:01 */ + struct tm tm; + struct tm *result = localtime_r (&t, &tm); + ASSERT (result == &tm); + ASSERT (result->tm_sec == 1); + ASSERT (result->tm_min == 0); + ASSERT (result->tm_hour == 2); + ASSERT (result->tm_mday == 28); + ASSERT (result->tm_mon == 10 - 1); + ASSERT (result->tm_year == 2007 - 1900); + ASSERT (result->tm_wday == 0); + ASSERT (result->tm_yday == 300); + ASSERT (result->tm_isdst == 0); +#if HAVE_STRUCT_TM_TM_GMTOFF + ASSERT (result->tm_gmtoff == (1 + result->tm_isdst) * 3600); +#endif +#if HAVE_STRUCT_TM_TM_ZONE + printf ("tm_zone = %s\n", result->tm_zone == NULL ? "(null)" : result->tm_zone); + ASSERT (strcmp (result->tm_zone, "CET") == 0); +#endif + } + + return 0; +} -- 2.34.1