blob: 2f3575e652ce5c1cf8e9240ca9b91b10912b6fdf [file]
#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