+/*
+ * Sanity check of libcdecl multithread safety.
+ *
+ * Copyright © 2024 Nick Bowler
+ *
+ * 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/>.
+ */
+
+#include <config.h>
+#include <assert.h>
+#include "cdecl-internal.h"
+#include "errmsg.h"
+#include "tap.h"
+
+/*
+ * Function called from output.c but not needed for error messaging.
+ */
+const char *cdecl__token_name(unsigned token)
+{
+ tap_bail_out("stub cdecl__token_name called");
+}
+
+/*
+ * Prior returned value from cdecl_get_error in the main thread.
+ */
+static const struct cdecl_error *thread1_err;
+
+/*
+ * Check that the error code and message matches expectations (noting that
+ * this application does not call setlocale to enable translations).
+ */
+static void check_simple_err(const struct cdecl_error *err, unsigned t,
+ unsigned exp_code, unsigned msg_id)
+{
+ static const char errmsgs[] = STRTAB_INITIALIZER;
+ const char *exp_msg;
+
+ if (!tap_result(err->code == exp_code, "thread[%u] err->code", t)) {
+ tap_diag("Failed, unexpected result");
+ tap_diag(" Received: %u", err->code);
+ tap_diag(" Expected: %u", exp_code);
+ }
+
+ exp_msg = &errmsgs[msg_id];
+ if (!tap_result(!strcmp(err->str, exp_msg), "thread[%u] err->str", t)) {
+ tap_diag("Failed, unexpected result");
+ tap_diag(" Received: %.*s", (int)strlen(exp_msg), err->str);
+ tap_diag(" Expected: %s", exp_msg);
+ }
+}
+
+static void thread2_func(void)
+{
+ const struct cdecl_error *err;
+
+ cdecl__errmsg(CDECL__ENOTYPE);
+ err = cdecl_get_error();
+
+ /*
+ * Ensure that the error returned in this new thread is distinct from
+ * the error returned in the main thread.
+ */
+ tap_diag("thread[2] err: %p", (void *)err);
+ tap_result(thread1_err != err, "thread[2] new state");
+
+ check_simple_err(err, 2, CDECL_ENOPARSE, CDECL__ENOTYPE);
+
+ tap_diag("thread[2] exit");
+}
+
+#if USE_POSIX_THREADS || USE_ISOC_AND_POSIX_THREADS
+#define THREAD_API "posix"
+#include <pthread.h>
+
+static void *thread2(void *p)
+{
+ thread2_func();
+ return 0;
+}
+
+static void run_thread2(void)
+{
+ pthread_t t;
+ int err;
+
+ if (!(err = pthread_create(&t, 0, thread2, 0)))
+ if (!(err = pthread_join(t, 0)))
+ return;
+
+ tap_bail_out("run_thread2 failed: %s", strerror(err));
+}
+
+#elif USE_ISOC_THREADS
+#define THREAD_API "isoc"
+#include <threads.h>
+
+static int thread2(void *p)
+{
+ thread2_func();
+ return 0;
+}
+
+static void run_thread2(void)
+{
+ thrd_t t;
+ if (thrd_create(&t, thread2, 0) == thrd_success)
+ if (thrd_join(t, 0) == thrd_success)
+ return;
+
+ tap_bail_out("run_thread2 failed");
+}
+
+#elif USE_WINDOWS_THREADS
+#define THREAD_API "windows"
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+
+static DWORD WINAPI thread2(LPVOID p)
+{
+ thread2_func();
+ return 0;
+}
+
+static void run_thread2(void)
+{
+ HANDLE h;
+ DWORD rc;
+
+ if ((h = CreateThread(NULL, 0, thread2, NULL, 0, &rc))) {
+ do {
+ if (GetExitCodeThread(h, &rc) && rc != STILL_ACTIVE) {
+ CloseHandle(h);
+ return;
+ }
+ } while (WaitForSingleObject(h, INFINITE) != WAIT_FAILED);
+ }
+
+ tap_bail_out("run_thread2 failed (%lu)", GetLastError());
+}
+#else
+#undef THREAD_API
+int main(void)
+{
+ tap_skip_all("multithreading support disabled");
+}
+#endif
+
+#ifdef THREAD_API
+int main(void)
+{
+ size_t test_live_allocations(void);
+ const struct cdecl_error *err;
+
+ tap_diag("using thread API: " THREAD_API);
+ tap_plan(9);
+
+ /* Simulate an error in the main thread. */
+ cdecl__errmsg(CDECL__ENOMEM);
+ thread1_err = cdecl_get_error();
+ tap_diag("thread[1] err: %p", (void *)thread1_err);
+ check_simple_err(thread1_err, 1, CDECL_ENOMEM, CDECL__ENOMEM);
+
+ run_thread2();
+
+ /*
+ * Back in the main thread, the error previously returned by
+ * cdecl_get_error() should still be valid.
+ */
+ check_simple_err(thread1_err, 1, CDECL_ENOMEM, CDECL__ENOMEM);
+
+ /*
+ * Moreover, cdecl_get_error should return the same pointer it did
+ * last time (undocumented implementation detail).
+ */
+ if (!tap_result((err = cdecl_get_error()) == thread1_err,
+ "thread[1] unchanged state"))
+ {
+ tap_diag("Failed, unexpected result");
+ tap_diag(" Received: %p", (void *)err);
+ tap_diag(" Expected: %p", (void *)thread1_err);
+ }
+
+ /*
+ * Main thread allocation should be the only one left.
+ */
+ tap_result(test_live_allocations() == 1, "thread cleanup");
+ tap_done();
+}
+#endif