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

Reply via email to