[libc-commits] [libc] [libc][test] Condition out tests that can't work on bare metal (PR #215830)
Simon Tatham via libc-commits
libc-commits at lists.llvm.org
Thu Aug 13 07:14:54 PDT 2026
https://github.com/statham-arm updated https://github.com/llvm/llvm-project/pull/215830
>From 94eb9ff095894da506d1c3003cff5e9d08984a8d Mon Sep 17 00:00:00 2001
From: Simon Tatham <simon.tatham at arm.com>
Date: Wed, 12 Aug 2026 13:16:22 +0100
Subject: [PATCH 1/5] [libc][test] Condition out tests that can't work on bare
metal
In bare-metal builds of libc, the `EXPECT_DEATH` macro may not be
defined. Also, `signal-macros.h` may not define the values needed for
the rest of `<signal.h>` to work. So tests that rely on either of
those things will fail to compile.
I've conditioned out the `EXPECT_DEATH` tests completely if
`EXPECT_DEATH` isn't defined. There's inherently no reliable way to
define it: you can't rely on finding out about segmentation faults by
a signal, because accessing memory outside valid C objects might
silently succeed (valid unused memory), or generate a CPU fault that
no kernel traps for you, or overwrite something important outside your
program.
The check for signals in `FPExceptMatcher.cpp` can be more lenient,
and just condition out the signal-handling code, leaving the test of
cumulative FP exception flags in place, so that the checker simply
returns "no signal was caught" unconditionally.
---
libc/test/UnitTest/FPExceptMatcher.cpp | 21 ++++++++++++++++++-
.../src/compiler/stack_chk_guard_test.cpp | 7 +++++++
libc/test/src/time/localtime_r_test.cpp | 2 ++
libc/test/src/time/localtime_test.cpp | 2 ++
4 files changed, 31 insertions(+), 1 deletion(-)
diff --git a/libc/test/UnitTest/FPExceptMatcher.cpp b/libc/test/UnitTest/FPExceptMatcher.cpp
index 10b6ff9f91da1..863811841124f 100644
--- a/libc/test/UnitTest/FPExceptMatcher.cpp
+++ b/libc/test/UnitTest/FPExceptMatcher.cpp
@@ -20,7 +20,15 @@
#include "hdr/types/fenv_t.h"
#include "src/__support/FPUtil/FEnvImpl.h"
#include <setjmp.h>
+
+// To make this test work on bare-metal targets without working signal.h, find
+// out if signal-macros.h did anything, before including the full signal.h. It
+// doesn't define a specific macro of the form HAVE_SIGNALS, so we just test
+// for one of the macros it _does_ define.
+#include "llvm-libc-macros/signal-macros.h"
+#ifdef __NSIGSET_WORDS
#include <signal.h>
+#endif
#if LIBC_TEST_HAS_MATCHERS()
@@ -37,27 +45,38 @@ namespace testing {
using sighandler_t = __sighandler_t *;
#endif
-static thread_local sigjmp_buf jumpBuffer;
static thread_local bool caughtExcept;
+#ifdef __NSIGSET_WORDS
+
+static thread_local sigjmp_buf jumpBuffer;
+
static void sigfpeHandler([[maybe_unused]] int sig) {
caughtExcept = true;
siglongjmp(jumpBuffer, -1);
}
+#endif // __NSIGSET_WORDS
+
FPExceptMatcher::FPExceptMatcher(FunctionCaller *func) {
+#ifdef __NSIGSET_WORDS
auto *oldSIGFPEHandler = signal(SIGFPE, &sigfpeHandler);
+#endif
caughtExcept = false;
fenv_t oldEnv;
fputil::get_env(&oldEnv);
+#ifdef __NSIGSET_WORDS
if (sigsetjmp(jumpBuffer, 1) == 0)
+#endif
func->call();
delete func;
// We restore the previous floating point environment after
// the call to the function which can potentially raise SIGFPE.
fputil::set_env(&oldEnv);
+#ifdef __NSIGSET_WORDS
signal(SIGFPE, oldSIGFPEHandler);
+#endif
exceptionRaised = caughtExcept;
}
diff --git a/libc/test/src/compiler/stack_chk_guard_test.cpp b/libc/test/src/compiler/stack_chk_guard_test.cpp
index 301031ff47bd5..978e889263405 100644
--- a/libc/test/src/compiler/stack_chk_guard_test.cpp
+++ b/libc/test/src/compiler/stack_chk_guard_test.cpp
@@ -10,6 +10,13 @@
#include "src/compiler/__stack_chk_fail.h"
#include "test/UnitTest/Test.h"
+#ifdef EXPECT_DEATH
TEST(LlvmLibcStackChkFail, Death) {
EXPECT_DEATH([] { __stack_chk_fail(); }, WITH_SIGNAL(SIGABRT));
}
+#else
+TEST(LlvmLibcStackChkFail, Dummy) {
+ // Need at least one test, because a completely empty test file
+ // counts as failure
+}
+#endif // EXPECT_DEATH
diff --git a/libc/test/src/time/localtime_r_test.cpp b/libc/test/src/time/localtime_r_test.cpp
index bc71419d68c64..e3c3b96278b89 100644
--- a/libc/test/src/time/localtime_r_test.cpp
+++ b/libc/test/src/time/localtime_r_test.cpp
@@ -27,6 +27,7 @@ TEST(LlvmLibcLocaltimeR, ValidUnixTimestamp0) {
ASSERT_EQ(0, result->tm_isdst);
}
+#ifdef EXPECT_DEATH
TEST(LlvmLibcLocaltimeR, NullPtr) {
struct tm input;
time_t timer = 0;
@@ -37,6 +38,7 @@ TEST(LlvmLibcLocaltimeR, NullPtr) {
EXPECT_DEATH([&] { LIBC_NAMESPACE::localtime_r(&timer, nullptr); },
WITH_SIGNAL(-1));
}
+#endif // EXPECT_DEATH
// TODO(zimirza): These tests does not expect the correct output of localtime as
// per specification. This is due to timezone functions removed from
diff --git a/libc/test/src/time/localtime_test.cpp b/libc/test/src/time/localtime_test.cpp
index 37974d27771dc..6e1116b98eeb1 100644
--- a/libc/test/src/time/localtime_test.cpp
+++ b/libc/test/src/time/localtime_test.cpp
@@ -25,9 +25,11 @@ TEST(LlvmLibcLocaltime, ValidUnixTimestamp0) {
ASSERT_EQ(0, result->tm_isdst);
}
+#ifdef EXPECT_DEATH
TEST(LlvmLibcLocaltime, NullPtr) {
EXPECT_DEATH([] { LIBC_NAMESPACE::localtime(nullptr); }, WITH_SIGNAL(-1));
}
+#endif // EXPECT_DEATH
// TODO(zimirza): These tests does not expect the correct output of localtime as
// per specification. This is due to timezone functions removed from
>From 64a74b3248cc92a09e55624ba3cb81a1f1dad8c0 Mon Sep 17 00:00:00 2001
From: Simon Tatham <simon.tatham at arm.com>
Date: Thu, 13 Aug 2026 12:47:25 +0100
Subject: [PATCH 2/5] Revised EXPECT_DEATH technique
---
libc/cmake/modules/LLVMLibCTestRules.cmake | 21 ++++++++++++-------
libc/test/UnitTest/LibcTest.h | 18 ++++++++--------
.../src/compiler/stack_chk_guard_test.cpp | 7 -------
libc/test/src/time/localtime_r_test.cpp | 2 --
libc/test/src/time/localtime_test.cpp | 2 --
5 files changed, 22 insertions(+), 28 deletions(-)
diff --git a/libc/cmake/modules/LLVMLibCTestRules.cmake b/libc/cmake/modules/LLVMLibCTestRules.cmake
index a6bd67bd33815..d5eeba86763de 100644
--- a/libc/cmake/modules/LLVMLibCTestRules.cmake
+++ b/libc/cmake/modules/LLVMLibCTestRules.cmake
@@ -4,6 +4,12 @@ else()
set(LIBC_TEST_SUBPROCESS_TESTS 0)
endif()
+if(LIBC_TARGET_OS_IS_BAREMETAL AND NOT DEFINED LIBC_TEST_SKIP_DEATH_TESTS)
+ # Bare-metal targets can't reliably test for crashes, without a kernel to
+ # catch CPU exceptions and turn them into signals.
+ set(LIBC_TEST_SKIP_DEATH_TESTS 1)
+endif()
+
function(_get_common_test_compile_options output_var c_test flags)
_get_compile_options_from_flags(compile_flags ${flags})
_get_compile_options_from_config(config_flags)
@@ -30,15 +36,14 @@ function(_get_common_test_compile_options output_var c_test flags)
libc_add_definition(compile_options
"LIBC_TEST_SUBPROCESS_TESTS=${LIBC_TEST_SUBPROCESS_TESTS}")
- if(LIBC_TEST_SUBPROCESS_TESTS)
- # EXPECT_DEATH and ASSERT_DEATH might be quite slow. LIBC_TEST_SKIP_DEATH_TESTS
- # will make those tests no-op to reduce the overall test time.
- if(LIBC_TEST_SKIP_DEATH_TESTS)
- if(LIBC_CMAKE_VERBOSE_LOGGING)
- message(STATUS "LIBC_TEST_SKIP_DEATH_TESTS is set. EXPECT_DEATH/ASSERT_DEATH are no-op.")
- endif()
- list(APPEND compile_options "-DLIBC_TEST_SKIP_DEATH_TESTS")
+ # Set LIBC_TEST_SKIP_DEATH_TESTS to skip running tests that use EXPECT_DEATH
+ # and ASSERT_DEATH. On platforms where they work, they can be slow; on
+ # bare-metal platforms it might not be possible to implement them at all.
+ if(LIBC_TEST_SKIP_DEATH_TESTS)
+ if(LIBC_CMAKE_VERBOSE_LOGGING)
+ message(STATUS "LIBC_TEST_SKIP_DEATH_TESTS is set. EXPECT_DEATH/ASSERT_DEATH are no-op.")
endif()
+ list(APPEND compile_options "-DLIBC_TEST_SKIP_DEATH_TESTS")
endif()
if(CMAKE_CROSSCOMPILING_EMULATOR)
diff --git a/libc/test/UnitTest/LibcTest.h b/libc/test/UnitTest/LibcTest.h
index 740d08335ab24..3e9d1266c6bee 100644
--- a/libc/test/UnitTest/LibcTest.h
+++ b/libc/test/UnitTest/LibcTest.h
@@ -494,7 +494,12 @@ CString libc_make_test_file_path_func(const char *file_name);
////////////////////////////////////////////////////////////////////////////////
// Subprocess checks.
-#if LIBC_TEST_SUBPROCESS_TESTS
+#ifdef LIBC_TEST_SKIP_DEATH_TESTS
+
+#define EXPECT_DEATH(FUNC, SIG)
+#define ASSERT_DEATH(FUNC, SIG)
+
+#elif LIBC_TEST_SUBPROCESS_TESTS
#define LIBC_TEST_PROCESS_(TEST_FUNC, FUNC, VALUE, RET_OR_EMPTY) \
LIBC_TEST_SCAFFOLDING_( \
@@ -507,19 +512,14 @@ CString libc_make_test_file_path_func(const char *file_name);
#define ASSERT_EXITS(FUNC, EXIT) \
LIBC_TEST_PROCESS_(testProcessExits, FUNC, EXIT, return)
-#ifdef LIBC_TEST_SKIP_DEATH_TESTS
-
-#define EXPECT_DEATH(FUNC, SIG)
-#define ASSERT_DEATH(FUNC, SIG)
-
-#else
-
#define EXPECT_DEATH(FUNC, SIG) \
LIBC_TEST_PROCESS_(testProcessKilled, FUNC, SIG, )
#define ASSERT_DEATH(FUNC, SIG) \
LIBC_TEST_PROCESS_(testProcessKilled, FUNC, SIG, return)
-#endif // LIBC_TEST_SKIP_DEATH_TESTS
+#else // not LIBC_TEST_SKIP_DEATH_TESTS or LIBC_TEST_SUBPROCESS_TESTS
+
+#error To run death tests provide a definition of EXPECT_DEATH and ASSERT_DEATH
#endif // LIBC_TEST_SUBPROCESS_TESTS
diff --git a/libc/test/src/compiler/stack_chk_guard_test.cpp b/libc/test/src/compiler/stack_chk_guard_test.cpp
index 978e889263405..301031ff47bd5 100644
--- a/libc/test/src/compiler/stack_chk_guard_test.cpp
+++ b/libc/test/src/compiler/stack_chk_guard_test.cpp
@@ -10,13 +10,6 @@
#include "src/compiler/__stack_chk_fail.h"
#include "test/UnitTest/Test.h"
-#ifdef EXPECT_DEATH
TEST(LlvmLibcStackChkFail, Death) {
EXPECT_DEATH([] { __stack_chk_fail(); }, WITH_SIGNAL(SIGABRT));
}
-#else
-TEST(LlvmLibcStackChkFail, Dummy) {
- // Need at least one test, because a completely empty test file
- // counts as failure
-}
-#endif // EXPECT_DEATH
diff --git a/libc/test/src/time/localtime_r_test.cpp b/libc/test/src/time/localtime_r_test.cpp
index e3c3b96278b89..bc71419d68c64 100644
--- a/libc/test/src/time/localtime_r_test.cpp
+++ b/libc/test/src/time/localtime_r_test.cpp
@@ -27,7 +27,6 @@ TEST(LlvmLibcLocaltimeR, ValidUnixTimestamp0) {
ASSERT_EQ(0, result->tm_isdst);
}
-#ifdef EXPECT_DEATH
TEST(LlvmLibcLocaltimeR, NullPtr) {
struct tm input;
time_t timer = 0;
@@ -38,7 +37,6 @@ TEST(LlvmLibcLocaltimeR, NullPtr) {
EXPECT_DEATH([&] { LIBC_NAMESPACE::localtime_r(&timer, nullptr); },
WITH_SIGNAL(-1));
}
-#endif // EXPECT_DEATH
// TODO(zimirza): These tests does not expect the correct output of localtime as
// per specification. This is due to timezone functions removed from
diff --git a/libc/test/src/time/localtime_test.cpp b/libc/test/src/time/localtime_test.cpp
index 6e1116b98eeb1..37974d27771dc 100644
--- a/libc/test/src/time/localtime_test.cpp
+++ b/libc/test/src/time/localtime_test.cpp
@@ -25,11 +25,9 @@ TEST(LlvmLibcLocaltime, ValidUnixTimestamp0) {
ASSERT_EQ(0, result->tm_isdst);
}
-#ifdef EXPECT_DEATH
TEST(LlvmLibcLocaltime, NullPtr) {
EXPECT_DEATH([] { LIBC_NAMESPACE::localtime(nullptr); }, WITH_SIGNAL(-1));
}
-#endif // EXPECT_DEATH
// TODO(zimirza): These tests does not expect the correct output of localtime as
// per specification. This is due to timezone functions removed from
>From 47235cd815582b85129b7d3b3dc8e7cfa87f3ce0 Mon Sep 17 00:00:00 2001
From: Simon Tatham <simon.tatham at arm.com>
Date: Thu, 13 Aug 2026 13:49:09 +0100
Subject: [PATCH 3/5] Revised technique for testing for signal.h
---
libc/test/CMakeLists.txt | 9 +++++++++
libc/test/UnitTest/FPExceptMatcher.cpp | 15 +++++----------
2 files changed, 14 insertions(+), 10 deletions(-)
diff --git a/libc/test/CMakeLists.txt b/libc/test/CMakeLists.txt
index dd8db9a5166c0..fd69b27b6500a 100644
--- a/libc/test/CMakeLists.txt
+++ b/libc/test/CMakeLists.txt
@@ -10,6 +10,15 @@ add_custom_target(libc-hermetic-tests-build)
add_custom_target(libc-integration-tests-build)
add_custom_target(libc_include_tests-build)
+# Allow compiled tests to check for the existence of particular headers in this
+# library build, by defining HAVE_STDIO_H, HAVE_SIGNAL_H etc to indicate that
+# they are present.
+foreach(target ${TARGET_PUBLIC_HEADERS})
+ string(REPLACE "libc.include." "" header ${target})
+ string(TOUPPER ${header} header)
+ add_compile_definitions(HAVE_${header}_H)
+endforeach()
+
# Configure the site config file for lit
configure_lit_site_cfg(
${LIBC_SOURCE_DIR}/test/lit.site.cfg.py.in
diff --git a/libc/test/UnitTest/FPExceptMatcher.cpp b/libc/test/UnitTest/FPExceptMatcher.cpp
index 863811841124f..72e53fe32ef4b 100644
--- a/libc/test/UnitTest/FPExceptMatcher.cpp
+++ b/libc/test/UnitTest/FPExceptMatcher.cpp
@@ -21,12 +21,7 @@
#include "src/__support/FPUtil/FEnvImpl.h"
#include <setjmp.h>
-// To make this test work on bare-metal targets without working signal.h, find
-// out if signal-macros.h did anything, before including the full signal.h. It
-// doesn't define a specific macro of the form HAVE_SIGNALS, so we just test
-// for one of the macros it _does_ define.
-#include "llvm-libc-macros/signal-macros.h"
-#ifdef __NSIGSET_WORDS
+#if HAVE_SIGNAL_H
#include <signal.h>
#endif
@@ -47,7 +42,7 @@ using sighandler_t = __sighandler_t *;
static thread_local bool caughtExcept;
-#ifdef __NSIGSET_WORDS
+#if HAVE_SIGNAL_H
static thread_local sigjmp_buf jumpBuffer;
@@ -59,14 +54,14 @@ static void sigfpeHandler([[maybe_unused]] int sig) {
#endif // __NSIGSET_WORDS
FPExceptMatcher::FPExceptMatcher(FunctionCaller *func) {
-#ifdef __NSIGSET_WORDS
+#if HAVE_SIGNAL_H
auto *oldSIGFPEHandler = signal(SIGFPE, &sigfpeHandler);
#endif
caughtExcept = false;
fenv_t oldEnv;
fputil::get_env(&oldEnv);
-#ifdef __NSIGSET_WORDS
+#if HAVE_SIGNAL_H
if (sigsetjmp(jumpBuffer, 1) == 0)
#endif
func->call();
@@ -74,7 +69,7 @@ FPExceptMatcher::FPExceptMatcher(FunctionCaller *func) {
// We restore the previous floating point environment after
// the call to the function which can potentially raise SIGFPE.
fputil::set_env(&oldEnv);
-#ifdef __NSIGSET_WORDS
+#if HAVE_SIGNAL_H
signal(SIGFPE, oldSIGFPEHandler);
#endif
exceptionRaised = caughtExcept;
>From a6fb9e2ce910aec8c7ff40db712ccea496c6b429 Mon Sep 17 00:00:00 2001
From: Simon Tatham <simon.tatham at arm.com>
Date: Thu, 13 Aug 2026 14:08:33 +0100
Subject: [PATCH 4/5] Remove the #error I just introduced
Apparently there are platforms which don't define
LIBC_TEST_SUBPROCESS_TESTS, which means that EXPECT_DEATH wouldn't
have been defined in that configuration at all. I assume those
platforms avoided a compile failure by not _calling_ EXPECT_DEATH. In
that case my #error was overzealous: it's only an error not to define
EXPECT_DEATH if you're also running a test that calls it.
---
libc/test/UnitTest/LibcTest.h | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/libc/test/UnitTest/LibcTest.h b/libc/test/UnitTest/LibcTest.h
index 3e9d1266c6bee..08aa2296714f0 100644
--- a/libc/test/UnitTest/LibcTest.h
+++ b/libc/test/UnitTest/LibcTest.h
@@ -517,11 +517,7 @@ CString libc_make_test_file_path_func(const char *file_name);
#define ASSERT_DEATH(FUNC, SIG) \
LIBC_TEST_PROCESS_(testProcessKilled, FUNC, SIG, return)
-#else // not LIBC_TEST_SKIP_DEATH_TESTS or LIBC_TEST_SUBPROCESS_TESTS
-
-#error To run death tests provide a definition of EXPECT_DEATH and ASSERT_DEATH
-
-#endif // LIBC_TEST_SUBPROCESS_TESTS
+#endif // LIBC_TEST_SKIP_DEATH_TESTS or LIBC_TEST_SUBPROCESS_TESTS
////////////////////////////////////////////////////////////////////////////////
// Custom matcher checks.
>From 5e0ad5f5fa8c73ee4e9bed12ce376f3518f47fde Mon Sep 17 00:00:00 2001
From: Simon Tatham <simon.tatham at arm.com>
Date: Thu, 13 Aug 2026 15:13:33 +0100
Subject: [PATCH 5/5] Expect the host libc to provide signal.h, if there is one
---
libc/test/UnitTest/FPExceptMatcher.cpp | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/libc/test/UnitTest/FPExceptMatcher.cpp b/libc/test/UnitTest/FPExceptMatcher.cpp
index 72e53fe32ef4b..f640bd7246bc4 100644
--- a/libc/test/UnitTest/FPExceptMatcher.cpp
+++ b/libc/test/UnitTest/FPExceptMatcher.cpp
@@ -21,7 +21,13 @@
#include "src/__support/FPUtil/FEnvImpl.h"
#include <setjmp.h>
-#if HAVE_SIGNAL_H
+// If LLVM libc is not providing <signal.h> and it's also not an overlay on
+// another libc that is, don't try to detect signals raised by FP exceptions.
+#if HAVE_SIGNAL_H || defined(LIBC_FULL_BUILD)
+#define TRY_TO_CATCH_SIGNALS
+#endif
+
+#ifdef TRY_TO_CATCH_SIGNALS
#include <signal.h>
#endif
@@ -42,7 +48,7 @@ using sighandler_t = __sighandler_t *;
static thread_local bool caughtExcept;
-#if HAVE_SIGNAL_H
+#ifdef TRY_TO_CATCH_SIGNALS
static thread_local sigjmp_buf jumpBuffer;
@@ -54,14 +60,14 @@ static void sigfpeHandler([[maybe_unused]] int sig) {
#endif // __NSIGSET_WORDS
FPExceptMatcher::FPExceptMatcher(FunctionCaller *func) {
-#if HAVE_SIGNAL_H
+#ifdef TRY_TO_CATCH_SIGNALS
auto *oldSIGFPEHandler = signal(SIGFPE, &sigfpeHandler);
#endif
caughtExcept = false;
fenv_t oldEnv;
fputil::get_env(&oldEnv);
-#if HAVE_SIGNAL_H
+#ifdef TRY_TO_CATCH_SIGNALS
if (sigsetjmp(jumpBuffer, 1) == 0)
#endif
func->call();
@@ -69,7 +75,7 @@ FPExceptMatcher::FPExceptMatcher(FunctionCaller *func) {
// We restore the previous floating point environment after
// the call to the function which can potentially raise SIGFPE.
fputil::set_env(&oldEnv);
-#if HAVE_SIGNAL_H
+#ifdef TRY_TO_CATCH_SIGNALS
signal(SIGFPE, oldSIGFPEHandler);
#endif
exceptionRaised = caughtExcept;
More information about the libc-commits
mailing list