| #include "exception_stack_tracer.hpp" | |
| #include "exception_stack_tracer_internal.hpp" | |
| #include <exception> | |
| #include <stdexcept> | |
| #include <string> | |
| #include <thread> // NOLINT | |
| #include <utility> | |
| #include "gtest/gtest.h" | |
| namespace { | |
| // Depth of the synthetic throw-site call chain. Comfortably exceeds the five | |
| // frames the capture test asserts, leaving margin under kMaxFrames. | |
| constexpr int kThrowDepth = 6; | |
| // Recurse to build a deep, real call stack and throw at the bottom, so the | |
| // capture test can assert the tracer recorded several throw-site frames rather | |
| // than just one. [[gnu::noinline]] keeps each level a distinct frame and the | |
| // post-call barrier defeats the tail call that would otherwise elide it. | |
| [[gnu::noinline]] void throwFromDepth(int depth) { | |
| if (depth <= 0) { | |
| throw std::runtime_error("boom-from-throw-site"); | |
| } | |
| throwFromDepth(depth - 1); | |
| asm volatile("" ::: "memory"); | |
| } | |
| // A distinct noinline entry so the throw site is not the test body. | |
| [[gnu::noinline]] void throwRuntimeError() { throwFromDepth(kThrowDepth); } | |
| // Throws and captures on a separate thread, so a later rethrow exercises the | |
| // cross-thread exception_ptr transport that asio/sdbusplus laundering uses. | |
| std::exception_ptr captureOnOtherThread() { | |
| std::exception_ptr eptr; | |
| std::thread thrower([&eptr] { | |
| try { | |
| throwRuntimeError(); | |
| } catch (...) { | |
| eptr = std::current_exception(); | |
| } | |
| }); | |
| thrower.join(); | |
| return eptr; | |
| } | |
| // Runs `fn` on a separate thread and lets any exception escape the thread's | |
| // entry function. An exception escaping a std::thread invokes std::terminate | |
| // directly -- this is the real uncaught path (asio io_context /sdbusplus posted | |
| // rethrow) and the ONLY way to reach our terminate handler from inside | |
| // EXPECT_DEATH: gtest wraps the death-test statement in a try/catch that would | |
| // otherwise intercept a main-thread throw before std::terminate ever runs. | |
| template <typename Fn> | |
| void terminateViaEscapingThread(Fn fn) { | |
| std::thread t(std::move(fn)); | |
| t.join(); // Not reached: the escaping exception terminates the process. | |
| } | |
| // All tests fork via EXPECT_DEATH and match the child's stderr, since the | |
| // behavior under test IS the abort. threadsafe style re-executes the test | |
| // binary for the child, which is required once threads are involved. | |
| TEST(ExceptionStackTracerDeathTest, UncaughtThrowPrintsThrowSiteStack) { | |
| testing::FLAGS_gtest_death_test_style = "threadsafe"; | |
| EXPECT_DEATH( | |
| { | |
| installExceptionStackTracer(); | |
| terminateViaEscapingThread(throwRuntimeError); | |
| }, | |
| "uncaught exception of type St13runtime_error" | |
| ".*what\\(\\): boom-from-throw-site" | |
| ".*throw-site stack \\(recorded as St13runtime_error\\)" | |
| // At least five frames, each resolved to a module+offset (not the | |
| // unresolved "[0x..]" fallback): proves a real multi-frame stack walk. | |
| ".*#00 [^ ]+\\+0x[0-9a-f]+" | |
| ".*#01 [^ ]+\\+0x[0-9a-f]+" | |
| ".*#02 [^ ]+\\+0x[0-9a-f]+" | |
| ".*#03 [^ ]+\\+0x[0-9a-f]+" | |
| ".*#04 [^ ]+\\+0x[0-9a-f]+"); | |
| } | |
| TEST(ExceptionStackTracerDeathTest, RethrownExceptionPtrKeepsThrowSiteStack) { | |
| testing::FLAGS_gtest_death_test_style = "threadsafe"; | |
| EXPECT_DEATH( | |
| { | |
| installExceptionStackTracer(); | |
| std::exception_ptr eptr = captureOnOtherThread(); | |
| // Uncaught rethrow on a third thread: the terminate happens on a | |
| // different thread than the throw, via a dependent exception -- the | |
| // record must still resolve through the primary object. | |
| terminateViaEscapingThread([eptr] { std::rethrow_exception(eptr); }); | |
| }, | |
| "what\\(\\): boom-from-throw-site" | |
| ".*throw-site stack \\(recorded as St13runtime_error\\)" | |
| ".*#00 "); | |
| } | |
| TEST(ExceptionStackTracerDeathTest, EvictedRecordDegradesGracefully) { | |
| testing::FLAGS_gtest_death_test_style = "threadsafe"; | |
| EXPECT_DEATH( | |
| { | |
| installExceptionStackTracer(); | |
| std::exception_ptr eptr = captureOnOtherThread(); | |
| // Wrap the 64-slot ring so the captured exception's record is gone. | |
| for (int i = 0; i < 100; ++i) { | |
| try { | |
| throw std::logic_error("filler"); | |
| } catch (const std::logic_error&) { | |
| } | |
| } | |
| terminateViaEscapingThread([eptr] { std::rethrow_exception(eptr); }); | |
| }, | |
| "what\\(\\): boom-from-throw-site" | |
| ".*throw-site stack unavailable"); | |
| } | |
| TEST(ExceptionStackTracerDeathTest, DisabledTracerKeepsDefaultTerminate) { | |
| testing::FLAGS_gtest_death_test_style = "threadsafe"; | |
| // libstdc++'s verbose terminate handler output, not ours. | |
| EXPECT_DEATH( | |
| { | |
| installExceptionStackTracer(false); | |
| terminateViaEscapingThread(throwRuntimeError); | |
| }, | |
| "terminate called after throwing an instance of"); | |
| } | |
| // In-process (non-death) tests. Unlike the death tests above -- whose forked | |
| // children abort() and so never flush coverage counters -- these run the | |
| // record, lookup, seqlock-validation and frame-printing logic in the parent | |
| // process via the extracted logThrowSiteStackOfCurrentException() helper. That | |
| // makes the terminate-path code visible to coverage and lets us assert on the | |
| // captured output directly instead of only matching a dying child's stderr. | |
| using exception_stack_tracer_internal::logThrowSiteStackOfCurrentException; | |
| using exception_stack_tracer_internal::resetForTest; | |
| TEST(ExceptionStackTracerTest, RecordsAndResolvesThrowSiteInProcess) { | |
| resetForTest(); | |
| installExceptionStackTracer(true); | |
| testing::internal::CaptureStderr(); | |
| try { | |
| throwRuntimeError(); | |
| } catch (...) { | |
| logThrowSiteStackOfCurrentException(); | |
| } | |
| const std::string err = testing::internal::GetCapturedStderr(); | |
| EXPECT_NE(err.find("uncaught exception of type St13runtime_error"), | |
| std::string::npos); | |
| EXPECT_NE(err.find("what(): boom-from-throw-site"), std::string::npos); | |
| EXPECT_NE(err.find("throw-site stack (recorded as St13runtime_error)"), | |
| std::string::npos); | |
| // At least five frames captured, proving a real multi-frame stack walk. | |
| for (const char* frame : {"#00 ", "#01 ", "#02 ", "#03 ", "#04 "}) { | |
| EXPECT_NE(err.find(frame), std::string::npos) << "missing frame " << frame; | |
| } | |
| } | |
| TEST(ExceptionStackTracerTest, DisabledTracerRecordsNothingInProcess) { | |
| resetForTest(); | |
| installExceptionStackTracer(false); | |
| testing::internal::CaptureStderr(); | |
| try { | |
| throwRuntimeError(); | |
| } catch (...) { | |
| logThrowSiteStackOfCurrentException(); | |
| } | |
| const std::string err = testing::internal::GetCapturedStderr(); | |
| EXPECT_NE(err.find("throw-site stack unavailable"), std::string::npos); | |
| } | |
| TEST(ExceptionStackTracerTest, EvictedRecordReportsUnavailableInProcess) { | |
| resetForTest(); | |
| installExceptionStackTracer(true); | |
| std::exception_ptr eptr = captureOnOtherThread(); | |
| // Wrap the 64-slot ring so the captured exception's record is overwritten. | |
| for (int i = 0; i < 100; ++i) { | |
| try { | |
| throw std::logic_error("filler"); | |
| } catch (const std::logic_error&) { | |
| } | |
| } | |
| testing::internal::CaptureStderr(); | |
| try { | |
| std::rethrow_exception(eptr); | |
| } catch (...) { | |
| logThrowSiteStackOfCurrentException(); | |
| } | |
| const std::string err = testing::internal::GetCapturedStderr(); | |
| EXPECT_NE(err.find("throw-site stack unavailable"), std::string::npos); | |
| } | |
| TEST(ExceptionStackTracerTest, NoActiveExceptionIsHandledInProcess) { | |
| resetForTest(); | |
| installExceptionStackTracer(true); | |
| testing::internal::CaptureStderr(); | |
| logThrowSiteStackOfCurrentException(); // no exception in flight | |
| const std::string err = testing::internal::GetCapturedStderr(); | |
| EXPECT_NE(err.find("called without an active exception"), std::string::npos); | |
| } | |
| } // namespace |