diff --git a/Libraries/LibTest/AssertionHandler.cpp b/Libraries/LibTest/AssertionHandler.cpp new file mode 100644 index 00000000000..9e97920f024 --- /dev/null +++ b/Libraries/LibTest/AssertionHandler.cpp @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025, ayeteadoe + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +namespace Test { + +static jmp_buf g_assert_jmp_buf = {}; + +static bool g_assert_jmp_buf_valid = false; + +jmp_buf& assertion_jump_buffer() { return g_assert_jmp_buf; } + +void set_assertion_jump_validity(bool validity) +{ + g_assert_jmp_buf_valid = validity; +} + +static bool is_assertion_jump_valid() +{ + return g_assert_jmp_buf_valid; +} + +static void assertion_handler_impl(char const*) +{ + if (is_assertion_jump_valid()) { + set_assertion_jump_validity(false); + LIBTEST_LONGJMP(assertion_jump_buffer(), 1); /* NOLINT(cert-err52-cpp, bugprone-setjmp-longjmp) Isolated to test infrastructure and allows us to not depend on spawning child processes for death tests */ + } + // Fall through to default assertion handler +} + +} + +#if defined(AK_OS_WINDOWS) +# define EXPORT __declspec(dllexport) +#else +# define EXPORT __attribute__((visibility("default"))) +#endif + +extern "C" EXPORT void ak_assertion_handler(char const* message); +void ak_assertion_handler(char const* message) +{ + ::Test::assertion_handler_impl(message); +} diff --git a/Libraries/LibTest/AssertionHandler.h b/Libraries/LibTest/AssertionHandler.h new file mode 100644 index 00000000000..abe963aef46 --- /dev/null +++ b/Libraries/LibTest/AssertionHandler.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025, ayeteadoe + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +#ifndef AK_OS_WINDOWS +# define LIBTEST_SETJMP(env) sigsetjmp(env, 1) +# define LIBTEST_LONGJMP siglongjmp +#else +# define LIBTEST_SETJMP(env) setjmp(env) +# define LIBTEST_LONGJMP longjmp +#endif + +namespace Test { + +jmp_buf& assertion_jump_buffer(); +void set_assertion_jump_validity(bool); +bool assertion_jump_validity(); + +} diff --git a/Libraries/LibTest/CMakeLists.txt b/Libraries/LibTest/CMakeLists.txt index 59c288c3614..1cb515666a1 100644 --- a/Libraries/LibTest/CMakeLists.txt +++ b/Libraries/LibTest/CMakeLists.txt @@ -1,4 +1,5 @@ -add_library(LibTestMain OBJECT TestMain.cpp) +add_library(LibTestMain OBJECT TestMain.cpp AssertionHandler.cpp) + target_link_libraries(LibTestMain PUBLIC GenericClangPlugin) add_library(JavaScriptTestRunnerMain OBJECT JavaScriptTestRunnerMain.cpp) diff --git a/Libraries/LibTest/Macros.h b/Libraries/LibTest/Macros.h index 0a088abcae6..c965995f5ed 100644 --- a/Libraries/LibTest/Macros.h +++ b/Libraries/LibTest/Macros.h @@ -7,11 +7,10 @@ #pragma once -#include #include #include #include -#include +#include #include #include #include @@ -185,29 +184,32 @@ consteval void expect_consteval(T) { } #define EXPECT_CONSTEVAL(...) ::Test::expect_consteval(__VA_ARGS__) -// To use, specify the lambda to execute in a sub process and verify it exits: -// EXPECT_CRASH("This should fail", []{ -// return Test::Crash::Failure::DidNotCrash; -// }); -#define EXPECT_CRASH(test_message, test_func) \ - do { \ - Test::Crash crash(test_message, test_func); \ - if (!crash.run()) \ - ::Test::set_current_test_result(::Test::TestResult::Failed); \ +#define EXPECT_DEATH(message, expression) \ + do { \ + ::Test::set_assertion_jump_validity(true); \ + if (LIBTEST_SETJMP(::Test::assertion_jump_buffer()) == 0) { \ + (expression); \ + ::Test::set_assertion_jump_validity(false); \ + if (::Test::is_reporting_enabled()) \ + ::AK::warnln("\033[31;1mFAIL\033[0m: {}:{}: EXPECT_DEATH({}) did not crash", __FILE__, __LINE__, message); \ + ::Test::set_current_test_result(::Test::TestResult::Failed); \ + } else { \ + ::Test::set_assertion_jump_validity(false); \ + } \ } while (false) -#define EXPECT_CRASH_WITH_SIGNAL(test_message, signal, test_func) \ - do { \ - Test::Crash crash(test_message, test_func, (signal)); \ - if (!crash.run()) \ - ::Test::set_current_test_result(::Test::TestResult::Failed); \ - } while (false) - -#define EXPECT_NO_CRASH(test_message, test_func) \ - do { \ - Test::Crash crash(test_message, test_func, 0); \ - if (!crash.run()) \ - ::Test::set_current_test_result(::Test::TestResult::Failed); \ +#define EXPECT_NO_DEATH(message, expression) \ + do { \ + ::Test::set_assertion_jump_validity(true); \ + if (LIBTEST_SETJMP(::Test::assertion_jump_buffer()) == 0) { \ + (expression); \ + ::Test::set_assertion_jump_validity(false); \ + } else { \ + ::Test::set_assertion_jump_validity(false); \ + if (::Test::is_reporting_enabled()) \ + ::AK::warnln("\033[31;1mFAIL\033[0m: {}:{}: EXPECT_NO_DEATH({}) crashed", __FILE__, __LINE__, message); \ + ::Test::set_current_test_result(::Test::TestResult::Failed); \ + } \ } while (false) #define TRY_OR_FAIL(expression) \ diff --git a/Libraries/LibTest/TestCase.h b/Libraries/LibTest/TestCase.h index 26f3934cb0f..b5c82c973f5 100644 --- a/Libraries/LibTest/TestCase.h +++ b/Libraries/LibTest/TestCase.h @@ -110,6 +110,7 @@ private: // Helper to hide implementation of TestSuite from users TEST_API void add_test_case_to_suite(NonnullRefPtr const& test_case); TEST_API void set_suite_setup_function(Function setup); + } #define TEST_SETUP \ diff --git a/Libraries/LibTest/TestSuite.cpp b/Libraries/LibTest/TestSuite.cpp index 0a2971cf87a..c04d8331478 100644 --- a/Libraries/LibTest/TestSuite.cpp +++ b/Libraries/LibTest/TestSuite.cpp @@ -12,8 +12,6 @@ #include #include #include -#include -#include namespace Test { diff --git a/Tests/AK/TestCharacterTypes.cpp b/Tests/AK/TestCharacterTypes.cpp index 87cc3941603..156619b9b95 100644 --- a/Tests/AK/TestCharacterTypes.cpp +++ b/Tests/AK/TestCharacterTypes.cpp @@ -151,44 +151,22 @@ TEST_CASE(parse_ascii_base36_digit) EXPECT_EQ(parse_ascii_base36_digit('Z'), 35u); EXPECT_EQ(parse_ascii_base36_digit('a'), 10u); EXPECT_EQ(parse_ascii_base36_digit('z'), 35u); - EXPECT_CRASH("parsing Base36 digit before valid numeric range", [] { - parse_ascii_base36_digit('/'); - return Test::Crash::Failure::DidNotCrash; - }); - EXPECT_CRASH("parsing Base36 digit after valid numeric range", [] { - parse_ascii_base36_digit(':'); - return Test::Crash::Failure::DidNotCrash; - }); - EXPECT_CRASH("parsing Base36 digit before valid uppercase range", [] { - parse_ascii_base36_digit('@'); - return Test::Crash::Failure::DidNotCrash; - }); - EXPECT_CRASH("parsing Base36 digit after valid uppercase range", [] { - parse_ascii_base36_digit('['); - return Test::Crash::Failure::DidNotCrash; - }); - EXPECT_CRASH("parsing Base36 digit before valid lowercase range", [] { - parse_ascii_base36_digit('`'); - return Test::Crash::Failure::DidNotCrash; - }); - EXPECT_CRASH("parsing Base36 digit after valid lowercase range", [] { - parse_ascii_base36_digit('{'); - return Test::Crash::Failure::DidNotCrash; - }); + + EXPECT_DEATH("parsing Base36 digit before valid numeric range", parse_ascii_base36_digit('/')); + EXPECT_DEATH("parsing Base36 digit after valid numeric range", parse_ascii_base36_digit(':')); + EXPECT_DEATH("parsing Base36 digit before valid uppercase range", parse_ascii_base36_digit('@')); + EXPECT_DEATH("parsing Base36 digit after valid uppercase range", parse_ascii_base36_digit('[')); + EXPECT_DEATH("parsing Base36 digit before valid lowercase range", parse_ascii_base36_digit('`')); + EXPECT_DEATH("parsing Base36 digit after valid lowercase range", parse_ascii_base36_digit('{')); } TEST_CASE(parse_ascii_digit) { EXPECT_EQ(parse_ascii_digit('0'), 0u); EXPECT_EQ(parse_ascii_digit('9'), 9u); - EXPECT_CRASH("parsing invalid ASCII digit", [] { - parse_ascii_digit('a'); - return Test::Crash::Failure::DidNotCrash; - }); - EXPECT_CRASH("parsing invalid unicode digit", [] { - parse_ascii_digit(0x00A9); - return Test::Crash::Failure::DidNotCrash; - }); + + EXPECT_DEATH("parsing invalid ASCII digit", parse_ascii_digit('a')); + EXPECT_DEATH("parsing invalid unicode digit", parse_ascii_digit(0x00A9)); } TEST_CASE(parse_ascii_hex_digit) @@ -196,10 +174,8 @@ TEST_CASE(parse_ascii_hex_digit) EXPECT_EQ(parse_ascii_hex_digit('0'), 0u); EXPECT_EQ(parse_ascii_hex_digit('F'), 15u); EXPECT_EQ(parse_ascii_hex_digit('f'), 15u); - EXPECT_CRASH("parsing invalid ASCII hex digit", [] { - parse_ascii_hex_digit('g'); - return Test::Crash::Failure::DidNotCrash; - }); + + EXPECT_DEATH("parsing invalid ASCII hex digit", parse_ascii_hex_digit('g')); } BENCHMARK_CASE(is_ascii) diff --git a/Tests/AK/TestFixedArray.cpp b/Tests/AK/TestFixedArray.cpp index 2fc853b2c7a..ec4f180e566 100644 --- a/Tests/AK/TestFixedArray.cpp +++ b/Tests/AK/TestFixedArray.cpp @@ -59,17 +59,16 @@ TEST_CASE(move) TEST_CASE(no_allocation) { FixedArray array = FixedArray::must_create_but_fixme_should_propagate_errors(5); - EXPECT_NO_CRASH("Assignments", [&] { + EXPECT_NO_DEATH("Assignments", [&]() { NoAllocationGuard guard; array[0] = 0; array[1] = 1; array[2] = 2; array[4] = array[1]; array[3] = array[0] + array[2]; - return Test::Crash::Failure::DidNotCrash; - }); + }()); - EXPECT_NO_CRASH("Move", [&] { + EXPECT_NO_DEATH("Move", [&]() { FixedArray moved_from_array = FixedArray::must_create_but_fixme_should_propagate_errors(6); // We need an Optional here to ensure that the NoAllocationGuard is // destroyed before the moved_to_array, because that would call free @@ -79,16 +78,13 @@ TEST_CASE(no_allocation) NoAllocationGuard guard; moved_to_array.emplace(move(moved_from_array)); } + }()); - return Test::Crash::Failure::DidNotCrash; - }); - - EXPECT_NO_CRASH("Swap", [&] { + EXPECT_NO_DEATH("Swap", [&]() { FixedArray target_for_swapping; { NoAllocationGuard guard; array.swap(target_for_swapping); } - return Test::Crash::Failure::DidNotCrash; - }); + }()); } diff --git a/Tests/AK/TestString.cpp b/Tests/AK/TestString.cpp index 89fd1c47935..010a36c5308 100644 --- a/Tests/AK/TestString.cpp +++ b/Tests/AK/TestString.cpp @@ -206,10 +206,7 @@ TEST_CASE(from_code_points) auto string = String::from_code_point(0x10ffff); EXPECT_EQ(string, "\xF4\x8F\xBF\xBF"sv); - EXPECT_CRASH("Creating a string from an invalid code point", [] { - String::from_code_point(0xffffffff); - return Test::Crash::Failure::DidNotCrash; - }); + EXPECT_DEATH("Creating a string from an invalid code point", (void)String::from_code_point(0xffffffff)); } TEST_CASE(substring) @@ -1147,10 +1144,7 @@ TEST_CASE(repeated) EXPECT_EQ(string3, "𐌀𐌀𐌀𐌀𐌀𐌀𐌀𐌀𐌀𐌀"sv); } - EXPECT_CRASH("Creating a string from an invalid code point", [] { - (void)String::repeated(0xffffffff, 1); - return Test::Crash::Failure::DidNotCrash; - }); + EXPECT_DEATH("Creating a string from an invalid code point", (void)String::repeated(0xffffffff, 1)); } TEST_CASE(join) diff --git a/Tests/AK/TestUtf16.cpp b/Tests/AK/TestUtf16.cpp index befa9bec19b..6f2fc98db26 100644 --- a/Tests/AK/TestUtf16.cpp +++ b/Tests/AK/TestUtf16.cpp @@ -178,15 +178,10 @@ TEST_CASE(iterate_utf16) EXPECT(iterator.length_in_code_units() == 2); EXPECT(++iterator == view.end()); - EXPECT_CRASH("Dereferencing Utf16CodePointIterator which is at its end.", [&iterator] { - *iterator; - return Test::Crash::Failure::DidNotCrash; - }); - EXPECT_CRASH("Incrementing Utf16CodePointIterator which is at its end.", [&iterator] { - ++iterator; - return Test::Crash::Failure::DidNotCrash; - }); + EXPECT_DEATH("Dereferencing Utf16CodePointIterator which is at its end.", *iterator); + + EXPECT_DEATH("Incrementing Utf16CodePointIterator which is at its end.", ++iterator); } TEST_CASE(validate_invalid_utf16) diff --git a/Tests/AK/TestUtf8.cpp b/Tests/AK/TestUtf8.cpp index c007cdd7e6c..927481a24cf 100644 --- a/Tests/AK/TestUtf8.cpp +++ b/Tests/AK/TestUtf8.cpp @@ -170,10 +170,8 @@ TEST_CASE(iterate_utf8) EXPECT(iterator.done()); EXPECT(!iterator.peek(0).has_value()); - EXPECT_CRASH("Dereferencing Utf8CodePointIterator which is already done.", [&iterator] { - *iterator; - return Test::Crash::Failure::DidNotCrash; - }); + + EXPECT_DEATH("Dereferencing Utf8CodePointIterator which is already done.", *iterator); } TEST_CASE(decode_invalid_ut8) diff --git a/Tests/LibTest/TestNoCrash.cpp b/Tests/LibTest/TestNoCrash.cpp index d29fad6db74..b37004cb185 100644 --- a/Tests/LibTest/TestNoCrash.cpp +++ b/Tests/LibTest/TestNoCrash.cpp @@ -8,7 +8,5 @@ TEST_CASE(raise) { - EXPECT_NO_CRASH("This should never crash", [] { - return Test::Crash::Failure::DidNotCrash; - }); + EXPECT_NO_DEATH("This should never crash", [] { }()); } diff --git a/Tests/LibXML/TestParser.cpp b/Tests/LibXML/TestParser.cpp index 62cf81b2122..8efaf9dfcd9 100644 --- a/Tests/LibXML/TestParser.cpp +++ b/Tests/LibXML/TestParser.cpp @@ -9,7 +9,7 @@ TEST_CASE(char_data_ending) { - EXPECT_NO_CRASH("parsing character data ending by itself should not crash", [] { + EXPECT_NO_DEATH("parsing character data ending by itself should not crash", [] { // After seeing ``, the parser will start parsing the content of the element. The content parser will then parse any character data it sees. // The character parser would see the first two `]]` and consume them. Then, it would see the `>` and set the state machine to say we have seen this, // but it did _not_ consume it and would instead tell GenericLexer that it should stop consuming characters. Therefore, we only consumed 2 characters. @@ -17,17 +17,15 @@ TEST_CASE(char_data_ending) // input when we only have 2 characters, causing an assertion failure as we are asking to take off more characters than there really is. XML::Parser parser("]]>"sv); (void)parser.parse(); - return Test::Crash::Failure::DidNotCrash; - }); + }()); } TEST_CASE(character_reference_integer_overflow) { - EXPECT_NO_CRASH("parsing character references that do not fit in 32 bits should not crash", [] { + EXPECT_NO_DEATH("parsing character references that do not fit in 32 bits should not crash", [] { XML::Parser parser("�"sv); (void)parser.parse(); - return Test::Crash::Failure::DidNotCrash; - }); + }()); } TEST_CASE(predefined_character_reference)