| #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 |