diff options
Diffstat (limited to 'src/zencore/testing.cpp')
| -rw-r--r-- | src/zencore/testing.cpp | 262 |
1 files changed, 250 insertions, 12 deletions
diff --git a/src/zencore/testing.cpp b/src/zencore/testing.cpp index 936424e0f..f5bc723b1 100644 --- a/src/zencore/testing.cpp +++ b/src/zencore/testing.cpp @@ -1,16 +1,143 @@ // Copyright Epic Games, Inc. All Rights Reserved. +#define ZEN_TEST_WITH_RUNNER 1 + #include "zencore/testing.h" + +#include "zencore/filesystem.h" #include "zencore/logging.h" +#include "zencore/process.h" +#include "zencore/trace.h" #if ZEN_WITH_TESTS -# include <doctest/doctest.h> +# include <zencore/callstack.h> + +# include <chrono> +# include <clocale> +# include <csignal> +# include <cstdlib> +# include <cstdio> +# include <string> +# include <vector> + +# if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC +# include <execinfo.h> +# include <unistd.h> +# elif ZEN_PLATFORM_WINDOWS +# include <crtdbg.h> +# endif namespace zen::testing { using namespace std::literals; +static void +PrintCrashCallstack([[maybe_unused]] const char* SignalName) +{ +# if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + // Use write() + backtrace_symbols_fd() which are async-signal-safe + write(STDERR_FILENO, "\n*** Caught ", 12); + write(STDERR_FILENO, SignalName, strlen(SignalName)); + write(STDERR_FILENO, " — callstack:\n", 15); + + void* Frames[64]; + int FrameCount = backtrace(Frames, 64); + backtrace_symbols_fd(Frames, FrameCount, STDERR_FILENO); +# elif ZEN_PLATFORM_WINDOWS + // On Windows we're called from SEH, not a signal handler, so heap/locks are safe + void* Addresses[64]; + uint32_t FrameCount = GetCallstack(2, 64, Addresses); + if (FrameCount > 0) + { + std::vector<std::string> Symbols = GetFrameSymbols(FrameCount, Addresses); + fprintf(stderr, "\n*** Caught %s - callstack:\n", SignalName); + for (uint32_t i = 0; i < FrameCount; ++i) + { + fprintf(stderr, " %2u: %s\n", i, Symbols[i].c_str()); + } + } +# endif +} + +# if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + +static void +CrashSignalHandler(int Signal) +{ + const char* SignalName = "Unknown signal"; + switch (Signal) + { + case SIGSEGV: + SignalName = "SIGSEGV"; + break; + case SIGABRT: + SignalName = "SIGABRT"; + break; + case SIGFPE: + SignalName = "SIGFPE"; + break; + case SIGBUS: + SignalName = "SIGBUS"; + break; + case SIGILL: + SignalName = "SIGILL"; + break; + } + + PrintCrashCallstack(SignalName); + + // Re-raise with default handler so the process terminates normally + signal(Signal, SIG_DFL); + raise(Signal); +} + +# endif // ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + +# if ZEN_PLATFORM_WINDOWS + +static LONG CALLBACK +CrashVectoredHandler(PEXCEPTION_POINTERS ExceptionInfo) +{ + // Only handle fatal exceptions, not first-chance exceptions used for normal control flow + switch (ExceptionInfo->ExceptionRecord->ExceptionCode) + { + case EXCEPTION_ACCESS_VIOLATION: + PrintCrashCallstack("EXCEPTION_ACCESS_VIOLATION"); + break; + case EXCEPTION_STACK_OVERFLOW: + PrintCrashCallstack("EXCEPTION_STACK_OVERFLOW"); + break; + case EXCEPTION_ILLEGAL_INSTRUCTION: + PrintCrashCallstack("EXCEPTION_ILLEGAL_INSTRUCTION"); + break; + case EXCEPTION_INT_DIVIDE_BY_ZERO: + PrintCrashCallstack("EXCEPTION_INT_DIVIDE_BY_ZERO"); + break; + default: + break; + } + + // Continue search so doctest's handler can report the test case context + return EXCEPTION_CONTINUE_SEARCH; +} + +# endif // ZEN_PLATFORM_WINDOWS + +static void +InstallCrashSignalHandlers() +{ +# if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + signal(SIGSEGV, CrashSignalHandler); + signal(SIGABRT, CrashSignalHandler); + signal(SIGFPE, CrashSignalHandler); + signal(SIGBUS, CrashSignalHandler); + signal(SIGILL, CrashSignalHandler); +# elif ZEN_PLATFORM_WINDOWS + AddVectoredExceptionHandler(0 /*called last among vectored handlers*/, CrashVectoredHandler); +# endif +} + struct TestListener : public doctest::IReporter { const std::string_view ColorYellow = "\033[0;33m"sv; @@ -21,9 +148,35 @@ struct TestListener : public doctest::IReporter void report_query(const doctest::QueryData& /*in*/) override {} - void test_run_start() override {} + void test_run_start() override { RunStart = std::chrono::steady_clock::now(); } + + void test_run_end(const doctest::TestRunStats& in) override + { + auto elapsed = std::chrono::steady_clock::now() - RunStart; + double elapsedSeconds = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count() / 1000.0; - void test_run_end(const doctest::TestRunStats& /*in*/) override {} + // Write machine-readable summary to file if requested (used by xmake test summary table) + const char* summaryFile = std::getenv("ZEN_TEST_SUMMARY_FILE"); + if (summaryFile && summaryFile[0] != '\0') + { + if (FILE* f = std::fopen(summaryFile, "w")) + { + std::fprintf(f, + "cases_total=%u\ncases_passed=%u\nassertions_total=%d\nassertions_passed=%d\n" + "elapsed_seconds=%.3f\n", + in.numTestCasesPassingFilters, + in.numTestCasesPassingFilters - in.numTestCasesFailed, + in.numAsserts, + in.numAsserts - in.numAssertsFailed, + elapsedSeconds); + for (const auto& failure : FailedTests) + { + std::fprintf(f, "failed=%s|%s|%u\n", failure.Name.c_str(), failure.File.c_str(), failure.Line); + } + std::fclose(f); + } + } + } void test_case_start(const doctest::TestCaseData& in) override { @@ -37,7 +190,14 @@ struct TestListener : public doctest::IReporter ZEN_CONSOLE("{}-------------------------------------------------------------------------------{}", ColorYellow, ColorNone); } - void test_case_end(const doctest::CurrentTestCaseStats& /*in*/) override { Current = nullptr; } + void test_case_end(const doctest::CurrentTestCaseStats& in) override + { + if (!in.testCaseSuccess && Current) + { + FailedTests.push_back({Current->m_name, Current->m_file.c_str(), Current->m_line}); + } + Current = nullptr; + } void test_case_exception(const doctest::TestCaseException& /*in*/) override {} @@ -57,7 +217,16 @@ struct TestListener : public doctest::IReporter void test_case_skipped(const doctest::TestCaseData& /*in*/) override {} - const doctest::TestCaseData* Current = nullptr; + const doctest::TestCaseData* Current = nullptr; + std::chrono::steady_clock::time_point RunStart = {}; + + struct FailedTestInfo + { + std::string Name; + std::string File; + unsigned Line; + }; + std::vector<FailedTestInfo> FailedTests; }; struct TestRunner::Impl @@ -75,20 +244,26 @@ TestRunner::~TestRunner() { } +void +TestRunner::SetDefaultSuiteFilter(const char* Pattern) +{ + m_Impl->Session.setOption("test-suite", Pattern); +} + int -TestRunner::ApplyCommandLine(int argc, char const* const* argv) +TestRunner::ApplyCommandLine(int Argc, char const* const* Argv) { - m_Impl->Session.applyCommandLine(argc, argv); + m_Impl->Session.applyCommandLine(Argc, Argv); - for (int i = 1; i < argc; ++i) + for (int i = 1; i < Argc; ++i) { - if (argv[i] == "--debug"sv) + if (Argv[i] == "--debug"sv) { - zen::logging::SetLogLevel(zen::logging::level::Debug); + zen::logging::SetLogLevel(zen::logging::Debug); } - else if (argv[i] == "--verbose"sv) + else if (Argv[i] == "--verbose"sv) { - zen::logging::SetLogLevel(zen::logging::level::Trace); + zen::logging::SetLogLevel(zen::logging::Trace); } } @@ -101,6 +276,69 @@ TestRunner::Run() return m_Impl->Session.run(); } +int +RunTestMain(int Argc, char* Argv[], const char* ExecutableName, void (*ForceLink)()) +{ +# if ZEN_PLATFORM_WINDOWS + setlocale(LC_ALL, "en_us.UTF8"); +# endif + + ForceLink(); + +# if ZEN_PLATFORM_LINUX + zen::IgnoreChildSignals(); +# endif + +# if ZEN_WITH_TRACE + zen::TraceInit(ExecutableName); + zen::TraceOptions TraceCommandlineOptions; + if (GetTraceOptionsFromCommandline(TraceCommandlineOptions)) + { + TraceConfigure(TraceCommandlineOptions); + } +# endif + +# if ZEN_PLATFORM_WINDOWS + // Suppress Windows error dialogs (crash/abort/assert) so tests terminate + // immediately instead of blocking on a modal dialog in CI or headless runs. + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + _set_abort_behavior(0, _WRITE_ABORT_MSG); + _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE); + _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR); + _CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_FILE); + _CrtSetReportFile(_CRT_ERROR, _CRTDBG_FILE_STDERR); +# endif + + zen::logging::InitializeLogging(); + zen::MaximizeOpenFileCount(); + InstallCrashSignalHandlers(); + + TestRunner Runner; + + // Derive default suite filter from ExecutableName: "zencore-test" -> "core.*" + if (ExecutableName) + { + std::string_view Name = ExecutableName; + if (Name.starts_with("zen")) + { + Name.remove_prefix(3); + } + if (Name.ends_with("-test")) + { + Name.remove_suffix(5); + } + if (!Name.empty()) + { + std::string Filter(Name); + Filter += ".*"; + Runner.SetDefaultSuiteFilter(Filter.c_str()); + } + } + + Runner.ApplyCommandLine(Argc, Argv); + return Runner.Run(); +} + } // namespace zen::testing #endif // ZEN_WITH_TESTS |